WHCSRL 技术网

Sharding-JDBC基本使用,Springboot实现分库分表,读写分离

一、Sharding-JDBC介绍

1、这里引用官网上的介绍:

定位为轻量级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、自己的理解:

增强版的JDBC驱动,客户端使用的时候,就像正常使用JDBC驱动一样, 引入Sharding-JDBC依赖包,连接好数据库,配置好分库分表规则,读写分离配置,然后客户端的sql 操作 Sharding-JDBC会自动根据配置完成 分库分表和读写分离操作。

二、实现效果

1、下图展示了我们通过Sharding-JDBC实现的分库分表及读写分离效果图

分库分表:结合上一篇的主从,这里我们使用上次搭建的主从数据库,3307的app1是主数据库,3308的app1是对应的从数据库。同时,我们在3307新建app2库和user2表,这里的app2库需要和app1库一样,user2表和user1表结构一样,主从会自动帮我们建表同步到3308,然后我们在项目中使用Sharding-JDBC 配置响应的分库分表策略,使得插入数据的时候 根据配置字段的分片规则将数据打入对应的库和表。在我们这里主要是 根据分库的分片规则决定数据进入3307的app1库还是app2库,然后再根据分表的分片规则决定进入user1表还是user2表。

读写分离:读写分离 在我们这里主要指的是 我们项目DQL会根据Sharding-JDBC配置的master-slave-rule走的3308的数据源,而项目的DML会根据master-slave-rule走3307的数据源

三、Spring-Boot项目整合Sharding-JDBC实现分库分表、读写分离

1、这里创建一个maven项目,首先引入依赖,pom.xml文件如下。

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <modelVersion>4.0.0</modelVersion>
  6. <parent>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-parent</artifactId>
  9. <version>2.3.3.RELEASE</version>
  10. </parent>
  11. <groupId>com.cgg</groupId>
  12. <artifactId>sharding-jdbc-test</artifactId>
  13. <version>1.0-SNAPSHOT</version>
  14. <properties>
  15. <java.version>1.8</java.version>
  16. </properties>
  17. <dependencies>
  18. <dependency>
  19. <groupId>org.springframework.boot</groupId>
  20. <artifactId>spring-boot-starter-web</artifactId>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter-test</artifactId>
  25. </dependency>
  26. <dependency>
  27. <groupId>mysql</groupId>
  28. <artifactId>mysql-connector-java</artifactId>
  29. <version>8.0.15</version>
  30. </dependency>
  31. <dependency>
  32. <groupId>com.alibaba</groupId>
  33. <artifactId>druid</artifactId>
  34. <version>1.1.21</version>
  35. </dependency>
  36. <dependency>
  37. <groupId>com.baomidou</groupId>
  38. <artifactId>mybatis-plus-boot-starter</artifactId>
  39. <version>3.1.1</version>
  40. </dependency>
  41. <dependency>
  42. <groupId>com.baomidou</groupId>
  43. <artifactId>mybatis-plus-extension</artifactId>
  44. <version>3.1.1</version>
  45. </dependency>
  46. <dependency>
  47. <groupId>org.apache.shardingsphere</groupId>
  48. <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
  49. <version>4.0.0-RC1</version>
  50. </dependency>
  51. <dependency>
  52. <groupId>org.projectlombok</groupId>
  53. <artifactId>lombok</artifactId>
  54. </dependency>
  55. </dependencies>
  56. <build>
  57. <plugins>
  58. <plugin>
  59. <groupId>org.springframework.boot</groupId>
  60. <artifactId>spring-boot-maven-plugin</artifactId>
  61. </plugin>
  62. </plugins>
  63. </build>
  64. </project>

注意:这里使用的是4.0的sharding-jdbc,spring-boot的版本是2.x的,在整合过程中遇见了许多问题,后面会有错误的解决步骤。

2、application.yml文件如下

  1. spring:
  2. jpa:
  3. properties:
  4. hibernate:
  5. hbm2ddl:
  6. auto: create
  7. dialect: org.hibernate.dialect.MySQL5Dialect
  8. show_sql: true
  9. shardingsphere:
  10. props:
  11. sql:
  12. show: true
  13. datasource:
  14. names: master0,master0slave0,master1,master1slave0
  15. master0:
  16. type: com.alibaba.druid.pool.DruidDataSource
  17. driver-class-name: com.mysql.cj.jdbc.Driver
  18. url: jdbc:mysql://127.0.0.1:3307/app1?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
  19. username: root
  20. password: 654321
  21. master1:
  22. type: com.alibaba.druid.pool.DruidDataSource
  23. driver-class-name: com.mysql.cj.jdbc.Driver
  24. url: jdbc:mysql://127.0.0.1:3307/app2?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
  25. username: root
  26. password: 654321
  27. master0slave0:
  28. type: com.alibaba.druid.pool.DruidDataSource
  29. driver-class-name: com.mysql.cj.jdbc.Driver
  30. url: jdbc:mysql://127.0.0.1:3308/app1?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT
  31. username: root
  32. password: 654321
  33. master1slave0:
  34. type: com.alibaba.druid.pool.DruidDataSource
  35. driver-class-name: com.mysql.cj.jdbc.Driver
  36. url: jdbc:mysql://127.0.0.1:3308/app2?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT
  37. username: root
  38. password: 654321
  39. sharding:
  40. default-database-strategy:
  41. inline:
  42. sharding-column: id
  43. algorithm-expression: app$->{(id %% 2)+1}
  44. tables:
  45. user:
  46. actual-data-nodes: app$->{1..2}.user$->{1..2}
  47. table-strategy:
  48. inline:
  49. sharding-column: id
  50. algorithm-expression: user$->{((""+id)[2..10].toInteger() %% 2)+1}
  51. key-generator:
  52. column: id
  53. type: SNOWFLAKE
  54. master-slave-rules:
  55. app1:
  56. master-data-source-name: master0
  57. slave-data-source-names: master0slave0
  58. app2:
  59. master-data-source-name: master1
  60. slave-data-source-names: master1slave0
  61. sharding:
  62. jdbc:
  63. config:
  64. masterslave:
  65. load-balance-algorithm-type: random

3、application.properties文件

  1. spring.main.allow-bean-definition-overriding=true
  2. mybatis-plus.mapper-locations= classpath:/mapper/*.xml
  3. mybatis-plus.configuration.log-impl= org.apache.ibatis.logging.stdout.StdOutImpl

4、分库分表实现

4.1、先说下数据源,结合之前mysql主从的文章,我本地127.0.0.1:3307端口是主,127.0.0.1:3308端口是从。

在3307下建立两个库app1和app2,同时每个库里面建立两张表user1和user2表,用来完成分库分表。

下面是app1库SQL语句:

  1. SET NAMES utf8mb4;
  2. SET FOREIGN_KEY_CHECKS = 0;
  3. -- ----------------------------
  4. -- Table structure for user1
  5. -- ----------------------------
  6. DROP TABLE IF EXISTS `user1`;
  7. CREATE TABLE `user1` (
  8. `id` bigint(11) NOT NULL COMMENT '主键id',
  9. `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  10. PRIMARY KEY (`id`) USING BTREE
  11. ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
  12. -- ----------------------------
  13. -- Table structure for user2
  14. -- ----------------------------
  15. DROP TABLE IF EXISTS `user2`;
  16. CREATE TABLE `user2` (
  17. `id` bigint(11) NOT NULL COMMENT '主键id',
  18. `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  19. PRIMARY KEY (`id`) USING BTREE
  20. ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
  21. SET FOREIGN_KEY_CHECKS = 1;

下面是app2库SQL语句:

  1. SET NAMES utf8mb4;
  2. SET FOREIGN_KEY_CHECKS = 0;
  3. -- ----------------------------
  4. -- Table structure for user1
  5. -- ----------------------------
  6. DROP TABLE IF EXISTS `user1`;
  7. CREATE TABLE `user1` (
  8. `id` bigint(11) NOT NULL COMMENT '主键id',
  9. `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  10. PRIMARY KEY (`id`) USING BTREE
  11. ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
  12. -- ----------------------------
  13. -- Table structure for user2
  14. -- ----------------------------
  15. DROP TABLE IF EXISTS `user2`;
  16. CREATE TABLE `user2` (
  17. `id` bigint(11) NOT NULL COMMENT '主键id',
  18. `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  19. PRIMARY KEY (`id`) USING BTREE
  20. ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
  21. SET FOREIGN_KEY_CHECKS = 1;

4.2、这里我们解释一下配置的分库分表规则实现将数据插入到app1和app2库,user1和user2表

  1. sharding:
  2. default-database-strategy:
  3. inline:
  4. sharding-column: id #分片的字段是id主键
  5. algorithm-expression: app$->{(id %% 2)+1} #分片的算法是 id对2求余然后加1
  6. tables:
  7. user:
  8. actual-data-nodes: app$->{1..2}.user$->{1..2} #实际的数据节点是(app1/app2).(user1/user2)
  9. table-strategy:
  10. inline:
  11. sharding-column: id #分表的分片字段是主键id
  12. algorithm-expression: user$->{((""+id)[2..10].toInteger() %% 2)+1} #分表的算法是取id的2-10位对2求余然后加1
  13. key-generator:
  14. column: id # 自动生成主键
  15. type: SNOWFLAKE # 生成主键的规则是雪花算法

上面配置的规则指的是,当有数据要插入数据库,或者进行查询时,sharding-jdbc通过分片配置的字段id的值,去根据配置的算法 进行运算,得到结果,例如上述分库规则,拿到id值 对2求余加1,那么不管id怎么变化算法返回的值永远是1和2,即app$->{(id %% 2)+1} 对应的就是app1和app2库,分表的规则是同样道理。

说明:这里只是配置了简单的分片规则来演示sharding-jdbc如何完成分库分表,我们也可以使用代码重写

方法来实现更复杂的分片策略。最后,这里的$->{(id %% 2)+1} 的{}中实际上是一个Groovy语法的表达式,sharding-jdbc是通过Groovy语法糖来解析分片策略的。所以想要配置更为复杂的策略,建议学一下Groovy语法。

4.3、接下来我们介绍配置的读写分离规则,如何实现读写分离

  1. master-slave-rules:
  2. app1: #分区 app1
  3. master-data-source-name: master0 #分区 app1的主数据源
  4. slave-data-source-names: master0slave0 #分区 app1的从数据源
  5. app2: #分区 app2
  6. master-data-source-name: master1 #分区 app2的主数据源
  7. slave-data-source-names: master1slave0 #分区 app2的从数据源

上面读写分离的规则指的是,分区app1的主从数据源,分区app2的主从数据源。至于这里的分区为什么是app1和app2?这里说明一下,我自己配置的时候,配置了几次都没有成功,一开始参照官网手册配置,以为分区名称可以自定义,于是配置的是ds0和ds1,但是项目启动报错了。报错信息是:

Cannot find data source in sharding rule, invalid actual data node is: 'app1.user1' 

开始以为是使用的sharding-jdbc版本问题,但是换了版本还是有问题,于是开始调试了一下源码:

从上面的截图中很明显就能发现,这里是要判断我们配置的分区集合也就是ds0和ds1是否包含 实际节点的数据源名称,也就是数据库名称。所以这里的分区名称是和我们上面配置的分片策略的数据库名称有关系的。

4.4、验证

接下来我们验证实际的效果。这里贴一下单元测试的代码。

  1. /**
  2. * @author cgg
  3. * @version 1.0.0
  4. * @date 2021/10/25
  5. */
  6. @SpringBootTest(classes = ShardingJdbcApp.class)
  7. @RunWith(SpringRunner.class)
  8. public class AppTest {
  9. @Resource
  10. private IUserService userService;
  11. /**
  12. * 测试sharding-jdbc添加数据
  13. */
  14. @Test
  15. public void testShardingJdbcInsert() {
  16. userService.InsertUser();
  17. }
  18. /**
  19. * 测试sharding-jdbc查询数据
  20. */
  21. @Test
  22. public void testShardingJdbcQuery() {
  23. //全部查询
  24. userService.queryUserList();
  25. //根据指定条件查询
  26. userService.queryUserById(1452619866473324545L);
  27. }
  28. }
  29. /**
  30. * @author cgg
  31. * @version 1.0.0
  32. * @date 2021/10/25
  33. */
  34. @Service
  35. @Slf4j
  36. public class UserServiceImpl implements IUserService {
  37. @Resource
  38. private UserMapper userMapper;
  39. @Resource
  40. private DataSource dataSource;
  41. @Override
  42. public List<User> queryUserList() {
  43. List<User> userList = userMapper.queryUserList();
  44. userList.forEach(user -> System.out.println(user.toString()));
  45. return userList;
  46. }
  47. @Override
  48. public User queryUserById(Long id) {
  49. User user = userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getId, id));
  50. System.out.println(user.toString());
  51. return user;
  52. }
  53. @Override
  54. public void InsertUser() {
  55. for (int i = 20; i < 40; i++) {
  56. User user = new User();
  57. user.setName("XX-" + i);
  58. int count = userMapper.insert(user);
  59. System.out.println(count);
  60. }
  61. }
  62. }

4.4.1、 首先看全部查询的结果

4.4.2、 再看下单条查询的结果

4.4.3、 再看下新增结果(实际插入到了主数据源的app1库user1表,并且后续每条插入都是走的主数据源,没有slave的操作)

四、问题及总结

到这里sharding-jdbc的初步基本使用已经没问题了,除了最简单的使用,我们还需要考虑分库分表后事务怎么处理,即分布式事务问题,还有读写分离后,数据不一致及同步延时问题,这些就需要我们从概念理论学习,然后在结合实际业务考虑方案。

推荐阅读