数据库读写分离
1. ShardingSphere
1.1 ShardingSphere 简介
ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar(计划中)这 3 款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。
ShardingSphere 定位为关系型数据库中间件,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。 它与 NoSQL 和 NewSQL 是并存而非互斥的关系。NoSQL 和 NewSQL 作为新技术探索的前沿,放眼未来,拥抱变化,是非常值得推荐的。反之,也可以用另一种思路看待问题,放眼未来,关注不变的东西,进而抓住事物本质。 关系型数据库当今依然占有巨大市场,是各个公司核心业务的基石,未来也难于撼动,我们目前阶段更加关注在原有基础上的增量,而非颠覆。
ShardingSphere 目前已经进入 Apache 孵化器, 欢迎通过 shardingsphere 的 dev 邮件列表与我们讨论。
1.2 Sharding-JDBC 简介
定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
- 适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
- 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。
- 支持任意实现 JDBC 规范的数据库。目前支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 标准的数据库。
2. 读写分离简介
2.1 背景
面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。 使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。
与将数据根据分片键打散至各个数据节点的水平分片不同,读写分离则是根据 SQL 语义的分析,将读操作和写操作分别路由至主库与从库。
读写分离的数据节点中的数据内容是一致的,而水平分片的每个数据节点的数据内容却并不相同。将水平分片和读写分离联合使用,能够更加有效的提升系统性能。
2.2 挑战
读写分离虽然可以提升系统的吞吐量和可用性,但同时也带来了数据不一致的问题。 这包括多个主库之间的数据一致性,以及主库与从库之间的数据一致性的问题。 并且,读写分离也带来了与数据分片同样的问题,它同样会使得应用开发和运维人员对数据库的操作和运维变得更加复杂。 下图展现了将分库分表与读写分离一同使用时,应用程序与数据库集群之间的复杂拓扑关系。
2.3 目标
透明化读写分离所带来的影响,让使用方尽量像使用一个数据库一样使用主从数据库集群,是 ShardingSphere 读写分离模块的主要设计目标。
2.4 核心概念
2.4.1 主库
添加、更新以及删除数据操作所使用的数据库,目前仅支持单主库
2.4.2 从库
查询数据操作所使用的数据库,可支持多从库。
2.4.3 主从同步
将主库的数据异步的同步到从库的操作。由于主从同步的异步性,从库与主库的数据会短时间内不一致。(这一步由数据库层实现,一般 RDS 数据库提供商,都会提供读写分离的服务)
2.4.4 负载均衡策略
通过负载均衡策略将查询请求疏导至不同从库。
2.5 核心功能
- 提供一主多从的读写分离配置,可独立使用,也可配合分库分表使用。
- 独立使用读写分离支持 SQL 透传。
- 同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性。
- 基于 Hint 的强制主库路由。
2.6 不支持项
- 主库和从库的数据同步。
- 主库和从库的数据同步延迟导致的数据不一致。
- 主库双写或多写。
3. 读写分离 Demo
3.1 添加 pom 文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.tao.learn</groupId> <artifactId>m1-7-sharding</artifactId> <version>0.0.1-SNAPSHOT</version> <name>sharding</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- JPA --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- postgresql --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>9.4.1212</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/sharding-jdbc-core --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-core</artifactId> <version>4.0.0-RC1</version> </dependency> <!-- for spring boot --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.0.0-RC1</version> </dependency> <!-- for spring namespace --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-namespace</artifactId> <version>4.0.0-RC1</version> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-transaction-xa-core</artifactId> <version>4.0.0-RC1</version> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.21</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.jetbrains</groupId> <artifactId>annotations</artifactId> <version>RELEASE</version> <scope>compile</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3.2 配置文件
Mysql 读写分离, 采用 Druid 作为 datasource
# Mysql读写分离, 采用Druid作为datasource spring.shardingsphere.datasource.names=master,slave0,slave1 spring.shardingsphere.datasource.master.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.master.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.master.url=jdbc:mysql://localhost:3306/master?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8 spring.shardingsphere.datasource.master.username=root spring.shardingsphere.datasource.master.password=123456 spring.shardingsphere.datasource.slave0.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.slave0.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.slave0.url=jdbc:mysql://localhost:3306/slave0?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8 spring.shardingsphere.datasource.slave0.username=root spring.shardingsphere.datasource.slave0.password=123456 spring.shardingsphere.datasource.slave1.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.slave1.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.slave1.url=jdbc:mysql://localhost:3306/slave1?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8 spring.shardingsphere.datasource.slave1.username=root spring.shardingsphere.datasource.slave1.password=123456 spring.shardingsphere.masterslave.name=ms spring.shardingsphere.masterslave.master-data-source-name=master spring.shardingsphere.masterslave.slave-data-source-names=slave0,slave1 spring.shardingsphere.props.sql.show=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
PostgreSQL 读写分离,采用 druid 作为 datasource
# PostgreSQL读写分离,采用druid作为datasource spring: shardingsphere: datasource: names: master,slave0,slave1 master: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/slave0 username: postgres password: postgres slave0: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/slave1 username: postgres password: postgres slave1: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/slave2 username: postgres password: postgres masterslave: load-balance-algorithm-type: round_robin name: ms master-data-source-name: master slave-data-source-names: slave0,slave1 props: sql.show: true jpa: properties: hibernate: dialect=org: hibernate: dialect: PostgreSQLDialect: org.hibernate.dialect.PostgreSQLDialect
PostgreSQL 读写分离,采用 hikari 作为 datasource
# PostgreSQL读写分离,采用hikari作为datasource spring: shardingsphere: datasource: names: master,slave0,slave1 master: type: com.zaxxer.hikari.HikariDataSource driver-class-name: org.postgresql.Driver jdbc-url: jdbc:postgresql://localhost:5432/master username: postgres password: postgres slave0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: org.postgresql.Driver jdbc-url: jdbc:postgresql://localhost:5432/slave0 username: postgres password: postgres slave1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: org.postgresql.Driver jdbc-url: jdbc:postgresql://localhost:5432/slave1 username: postgres password: postgres masterslave: load-balance-algorithm-type: round_robin name: ms master-data-source-name: master slave-data-source-names: slave0,slave1 props: sql.show: true jpa: properties: hibernate: temp: use_jdbc_metadata_defaults: false dialect: org.hibernate.dialect.PostgreSQLDialect
PostgreSQL 采用 hikari 作为 datasource,datasource 下的 url 改为
jdbc-url
application.yml
spring: jpa: properties: hibernate: hbm2ddl: auto: update show_sql: true profiles: active: master-slave-postgresql-hikari main: allow-bean-definition-overriding: true
3.3 编写 CRUD 基本操作
entity
/** * @author Jiantao Yan * @description: User实体类 * @title: UserServiceImpl * @date 2019/12/24 15:03 */ @Data @Entity @Table(name = "tb_user") @EntityListeners(AuditingEntityListener.class) public class User { /** * id */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** * 姓名 */ private String name; /** * 年龄 */ private Integer age; /** * 性别 */ private Integer sex; /** * 创建时间 */ @CreatedDate private LocalDateTime createTime; /** * 更新时间 */ @LastModifiedDate private LocalDateTime updateTime; }
dao
/** * @author Jiantao Yan * @description: 用户dao层 * @title: UserServiceImpl * @date 2019/12/24 15:03 */ @Component public interface UserDao extends JpaRepository<User, Long> { }
service
/** * @author Jiantao Yan * @description: User Service接口层 * @title: UserService * @date 2019/12/24 15:03 */ public interface UserService { List<User> findAll(); User save(User user); void update(User user); void delete(Long id); List<User> transaction(User user); }
/** * @author Jiantao Yan * @description: User Service 实现层 * @title: UserServiceImpl * @date 2019/12/24 15:03 */ @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override public List<User> findAll() { return userDao.findAll(); } @Override public User save(User user) { return userDao.save(user); } @Override public void update(User user) { userDao.save(user); } @Override public void delete(Long id) { userDao.deleteById(id); } @Override @Transactional(rollbackFor = Exception.class) public List<User> transaction(User user) { userDao.deleteById(user.getId()); return userDao.findAll(); } }
controller
/** * @author Jiantao Yan * @description: 用户Controller层 * @title: UserController * @date 2019/12/24 15:13 */ @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @GetMapping("/") public List<User> findAll() { return userService.findAll(); } @GetMapping("/transaction") public List<User> transaction(@RequestBody User user) { return userService.transaction(user); } @PostMapping("/") public User save(@RequestBody User user) { return userService.save(user); } @PutMapping("/") public Object update(User user) { userService.update(user); Map<String, Object> result = new HashMap<>(2); result.put("code", 200); result.put("msg", "更新成功"); return result; } @DeleteMapping("/{id}") public Object save(@PathVariable(name = "id") Long id) { userService.delete(id); Map<String, Object> result = new HashMap<>(2); result.put("code", 200); result.put("msg", "更新成功"); return result; } }
3.4 SQL
MySql 建表语句
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for tb_user -- ---------------------------- DROP TABLE IF EXISTS `tb_user`; CREATE TABLE `tb_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '名字', `age` mediumint(9) NOT NULL DEFAULT 0 COMMENT '年龄', `sex` tinyint(4) NOT NULL DEFAULT 0 COMMENT '性别:0无,1男,2女', `create_time` datetime(0) NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间', `update_time` datetime(0) NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 398790076873572353 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表0' ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
PostgreSql 建表语句
-- ---------------------------- -- Table structure for tb_user -- ---------------------------- DROP TABLE IF EXISTS "public"."tb_user"; CREATE TABLE "public"."tb_user" ( "id" int8 NOT NULL DEFAULT nextval('tb_user_id_seq'::regclass), "age" int4, "create_time" timestamp(6), "name" varchar(255) COLLATE "pg_catalog"."default", "sex" int4, "update_time" timestamp(6) ) ; -- ---------------------------- -- Primary Key structure for table tb_user -- ---------------------------- ALTER TABLE "public"."tb_user" ADD CONSTRAINT "tb_user_pkey" PRIMARY KEY ("id");
3.5 测试
3.5.1 插入操作
使用主库进行插入操作
2020-01-03 10:33:34.532 INFO 2584 --- [nio-8080-exec-2] ShardingSphere-SQL : Rule Type: master-slave 2020-01-03 10:33:34.532 INFO 2584 --- [nio-8080-exec-2] ShardingSphere-SQL : SQL: insert into tb_user (age, create_time, name, sex, update_time) values (?, ?, ?, ?, ?) ::: DataSources: master Hibernate: select currval('tb_user_id_seq') 2020-01-03 10:33:34.545 INFO 2584 --- [nio-8080-exec-2] ShardingSphere-SQL : Rule Type: master-slave 2020-01-03 10:33:34.546 INFO 2584 --- [nio-8080-exec-2] ShardingSphere-SQL : SQL: select currval('tb_user_id_seq') ::: DataSources: master
3.5.2 查询操作
使用从库查询,并且两个从库进行轮询
2020-01-03 10:33:34.545 INFO 2584 --- [nio-8080-exec-2] ShardingSphere-SQL : Rule Type: master-slave 2020-01-03 10:33:34.546 INFO 2584 --- [nio-8080-exec-2] ShardingSphere-SQL : SQL: select currval('tb_user_id_seq') ::: DataSources: master Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.create_time as create_t3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_, user0_.update_time as update_t6_0_ from tb_user user0_ 2020-01-03 10:36:23.344 INFO 2584 --- [nio-8080-exec-5] ShardingSphere-SQL : Rule Type: master-slave 2020-01-03 10:36:23.345 INFO 2584 --- [nio-8080-exec-5] ShardingSphere-SQL : SQL: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.create_time as create_t3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_, user0_.update_time as update_t6_0_ from tb_user user0_ ::: DataSources: slave1 Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.create_time as create_t3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_, user0_.update_time as update_t6_0_ from tb_user user0_ 2020-01-03 10:36:57.317 INFO 2584 --- [nio-8080-exec-6] ShardingSphere-SQL : Rule Type: master-slave 2020-01-03 10:36:57.317 INFO 2584 --- [nio-8080-exec-6] ShardingSphere-SQL : SQL: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.create_time as create_t3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_, user0_.update_time as update_t6_0_ from tb_user user0_ ::: DataSources: slave0
3.5.3 事务操作
该线程只要有一个是增删改,则该线程的后续操作全部采用主库 master
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.create_time as create_t3_0_0_, user0_.name as name4_0_0_, user0_.sex as sex5_0_0_, user0_.update_time as update_t6_0_0_ from tb_user user0_ where user0_.id=? 2020-01-03 10:39:20.562 INFO 2584 --- [nio-8080-exec-3] ShardingSphere-SQL : Rule Type: master-slave 2020-01-03 10:39:20.563 INFO 2584 --- [nio-8080-exec-3] ShardingSphere-SQL : SQL: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.create_time as create_t3_0_0_, user0_.name as name4_0_0_, user0_.sex as sex5_0_0_, user0_.update_time as update_t6_0_0_ from tb_user user0_ where user0_.id=? ::: DataSources: slave1 Hibernate: delete from tb_user where id=? 2020-01-03 10:39:20.565 INFO 2584 --- [nio-8080-exec-3] ShardingSphere-SQL : Rule Type: master-slave 2020-01-03 10:39:20.565 INFO 2584 --- [nio-8080-exec-3] ShardingSphere-SQL : SQL: delete from tb_user where id=? ::: DataSources: master Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.create_time as create_t3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_, user0_.update_time as update_t6_0_ from tb_user user0_ 2020-01-03 10:39:20.567 INFO 2584 --- [nio-8080-exec-3] ShardingSphere-SQL : Rule Type: master-slave 2020-01-03 10:39:20.567 INFO 2584 --- [nio-8080-exec-3] ShardingSphere-SQL : SQL: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.create_time as create_t3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_, user0_.update_time as update_t6_0_ from tb_user user0_ ::: DataSources: master
全部使用主库的前提的要有一个增删改操作,后续才使用主库,而不是所有的事务操作都会使用主库
例如:
查-> 改-> 删-> 查
该操作所对应的数据库为:从-> 主-> 主-> 主
改-> 查-> 删-> 增-> 查
该操作所对应的数据库为:主-> 主-> 主-> 主-> 主
4. 结语
采用 sharding-jdbc
做读写分离的好处有以下几点:
- 对项目代码零侵入,不用修改业务层的代码
- 从库可设置路由策略
- 可独立使用,也可配合以后分库分表作为使用
- 同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性
缺点:
- 主库读写策略不是根据事务来划分,而是根据
同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性
- 主从库数据不支持(不影响)
- 主库双写或多写(不影响)
源码位置:learn
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于