Spring Boot整合Sharding-JDBC实现数据分库分表

需求

产品需求:数据增量快,且数据总量大

软件需求:性能满足压测标准

  1. 能够实现分库分表
  2. 能够有一定的自由度,可做定制化开发
  3. 性能损耗小
  4. 易于开发

技术选型

Sharding-JDBC

优点

  1. 框架轻量级
  2. 以jar包形式提供服务,无需额外部署和依赖
  3. 完全兼容JDBC和各种ORM框架
  4. 性能损耗小

缺点

  1. 仅面向开发人员(DBA不感知),对代码有较小的侵入

概要介绍

官网:Apache ShardingSphere

文档:3.X版本官方文档

配置方式:Spring Boot Starter

由于预研过程中,发现3.1.0及以上版本与Gaea6.11.X存在无法解决的包冲突,因此最终使用的版本为3.0.0,以下文字说明及示例代码均基于sharding-jdbc 3.0.0版本

Maven依赖

  1. <!--sharding-jdbc-->
  2. <dependency>
  3. <groupId>io.shardingsphere</groupId>
  4. <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
  5. <version>3.0.0</version>
  6. </dependency>
  7. <!--阿里数据库连接池(可选)-->
  8. <dependency>
  9. <groupId>com.alibaba</groupId>
  10. <artifactId>druid</artifactId>
  11. <version>1.1.21</version>
  12. </dependency>

yaml配置

官方文档很良心,基本说明移步Spring Boot配置

  1. #配置均支持Groovy语法,详情移步百度
  2. sharding:
  3. jdbc:
  4. datasource:
  5. #数据库名,以英文逗号分隔
  6. names: habit_local,habit_local_0,habit_local_1,habit_local_fj
  7. #默认库,即不需要分库或分表的数据表所在的库。如habit表在该库,而user_habit_u和user_habit_h不在该库中
  8. habit_local:
  9. type: com.alibaba.druid.pool.DruidDataSource
  10. driver: com.mysql.jdbc.Driver
  11. #参照隔壁组前车之鉴,与ND的fabric驱动无法适配,具体说明:http://dwz.date/36U
  12. url: jdbc:mysql://127.0.0.1:3306/habit_local?autoReconnect=true&useUnicode=true&characterEncoding=UTF8
  13. username: root
  14. password: XXX
  15. driver-class-name: com.mysql.jdbc.Driver
  16. #按标准分库策略分配的库
  17. habit_local_0:
  18. type: com.alibaba.druid.pool.DruidDataSource
  19. driver: com.mysql.jdbc.Driver
  20. url: jdbc:mysql://127.0.0.1:3306/habit_local_0?autoReconnect=true&useUnicode=true&characterEncoding=UTF8
  21. username: root
  22. password: XXX
  23. driver-class-name: com.mysql.jdbc.Driver
  24. habit_local_1:
  25. type: com.alibaba.druid.pool.DruidDataSource
  26. driver: com.mysql.jdbc.Driver
  27. url: jdbc:mysql://127.0.0.1:3306/habit_local_1?autoReconnect=true&useUnicode=true&characterEncoding=UTF8
  28. username: root
  29. password: XXX
  30. driver-class-name: com.mysql.jdbc.Driver
  31. #为某产品独立配置的库
  32. habit_local_fj:
  33. type: com.alibaba.druid.pool.DruidDataSource
  34. driver: com.mysql.jdbc.Driver
  35. url: jdbc:mysql://127.0.0.1:3306/habit_local_fj?autoReconnect=true&useUnicode=true&characterEncoding=UTF8
  36. username: root
  37. password: XXX
  38. driver-class-name: com.mysql.jdbc.Driver
  39. config:
  40. sharding:
  41. props:
  42. sql:
  43. show: true
  44. tables:
  45. #虚拟表名称
  46. user_habit_u:
  47. #必须完整书写库表
  48. actual-data-nodes: habit_local_$->{0..1}.user_habit_u_$->{0..2},habit_local_fj.user_habit_u_$->{0..2}
  49. #分库策略
  50. database-strategy:
  51. #标准策略
  52. standard:
  53. #分库依据
  54. sharding-column: tenant_id
  55. #指定策略实现
  56. precise-algorithm-class-name: com.nd.elearning.habit.cultivate.sdk.api.config.DatabasePreciseShardingConfig
  57. #分表策略
  58. table-strategy:
  59. #内联行表达式
  60. inline:
  61. #分表依据
  62. sharding-column: user_id
  63. #策略表达式,只支持基础的取模和hash
  64. algorithm-expression: user_habit_u_$->{user_id % 3}
  65. user_habit_h:
  66. actual-data-nodes: habit_local_$->{0..1}.user_habit_h_$->{0..2},habit_local_fj.user_habit_u_$->{0..2}
  67. database-strategy:
  68. standard:
  69. sharding-column: tenant_id
  70. precise-algorithm-class-name: com.nd.elearning.habit.cultivate.sdk.api.config.DatabasePreciseShardingConfig
  71. table-strategy:
  72. standard:
  73. sharding-column: habit_id
  74. precise-algorithm-class-name: com.nd.elearning.habit.cultivate.sdk.api.config.UserHabitPreciseShardingConfig
  75. #没有进行分片存取的表所查询的默认数据库
  76. default-data-source-name: habit_local
  77. #以下为自定义的配置,用于解决为某产品单独配置库的需求
  78. custom:
  79. independence-app: {5: habit_local_fj}

Java代码

库/表策略类

  1. /**
  2. * UserHabit表按habit_id分片的配置.
  3. * <p>Description: </p>
  4. * <p>Create Time: 2020/2/19 0019</p>
  5. * @author 910204(zys)
  6. */
  7. public class UserHabitPreciseShardingConfig implements PreciseShardingAlgorithm {
  8. /**
  9. * 精确分片(分表)算法
  10. *
  11. * @param availableTargetNames 表名称列表
  12. * @param shardingValue 分片的列的值
  13. * @return String 表名
  14. */
  15. @Override
  16. public String doSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) {
  17. int suffix =
  18. Math.abs(shardingValue.getValue().toString().hashCode() % availableTargetNames.size());
  19. // 由于纯数字后缀匹配有可能table_22被2匹配到,因此后缀要加上自定义的分隔符,如"_"
  20. String suffixStr = "_" + String.valueOf(suffix);
  21. for (Object each : availableTargetNames) {
  22. String targetName = ((String) each);
  23. if (targetName.endsWith(suffixStr)) {
  24. return targetName;
  25. }
  26. }
  27. return availableTargetNames.iterator().next().toString();
  28. }
  29. }

JPA Entity

  1. @Data
  2. @Entity
  3. @Table(name = "user_habit_u")
  4. public class UserHabitByUser {
  5. @Id
  6. @Column(nullable = false, length = 36)
  7. @Type(type = "uuid-char")
  8. private UUID userHabitId;
  9. @Column
  10. private Long tenantId;
  11. @Column
  12. private Long userId;
  13. //其余字段
  14. }

Repository

以下三种查询方式均支持,sharding-jdbc语法解析器会完成虚拟表到物理表的映射

  1. public interface UserHabitByUserRepository extends JpaRepository<UserHabitByUser, UUID> {
  2. List<UserHabitByUser> findAllByUserId(Long userId);
  3. @Query(value = "select uhu from UserHabitByUser uhu where uhu.userId=:userId")
  4. List<UserHabitByUser> getUserAll(@Param(value = "userId") Long userId);
  5. @Query(value = "select uhu.* from user_habit_u uhu where uhu.user_id=:userId",nativeQuery = true)
  6. List<UserHabitByUser> getUserAll2(@Param(value = "userId") Long userId);
  7. }

自定义代码部分

  1. // 读取yaml中对象
  2. @Data
  3. // 交由spring托管
  4. @Component
  5. @ConfigurationProperties(prefix = "sharding.custom")
  6. // 类名无所谓
  7. public class ShardingCustomConfig {
  8. // 字段名需驼峰匹配
  9. private Map<String, String> independenceApp;
  10. }

PS:自动映射yaml配置到对象非spring boot本身功能,需集成以下包。(sharding-jdbc-spring-boot-starter自带了)

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-configuration-processor</artifactId>
  4. <optional>true</optional>
  5. </dependency>
  1. public class DatabasePreciseShardingConfig implements PreciseShardingAlgorithm {
  2. /**
  3. * 分库算法
  4. *
  5. * @param availableTargetNames 库名称列表
  6. * @param shardingValue 分库的列的名称
  7. * @return String 库名
  8. */
  9. @Override
  10. public String doSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) {
  11. // 从上下文中获取(我在该类中@Resource无法注入,但是从上下文中可以,很奇怪)
  12. ShardingCustomConfig shardingCustomConfig =
  13. ApplicationContextUtil.getApplicationContext().getBean(ShardingCustomConfig.class);
  14. // 获取单独部署库的租户
  15. Set<String> independenceAppTenantIds = shardingCustomConfig.getIndependenceApp().keySet();
  16. // 若未需单独部署的租户,直接映射到配置的库中
  17. if (independenceAppTenantIds.contains(shardingValue.getValue().toString())) {
  18. return shardingCustomConfig.getIndependenceApp().get(shardingValue.getValue().toString());
  19. }
  20. // 其他租户按指定策略寻库
  21. int suffix = Math.abs(shardingValue.getValue().toString().hashCode() % 2);
  22. // 由于纯数字后缀匹配有可能db_22被2匹配到,因此后缀要加上自定义的分隔符,如"_"
  23. String suffixStr = "_" + String.valueOf(suffix);
  24. for (Object each : availableTargetNames) {
  25. String targetName = ((String) each);
  26. if (targetName.endsWith(suffixStr)) {
  27. return targetName;
  28. }
  29. }
  30. return availableTargetNames.iterator().next().toString();
  31. }
  32. }

注意事项及思考

  • 若仅进行分表,查询被分表的表时,必须带上分表依据的列,否则sharding-jdbc默认回去遍历所有的分表。如查询user_habit_u虚拟表时,不带user_id条件,则引擎会去依次查询user_habit_u_0、user_habit_u_1、user_habit_u_2,然后返回查询结果
  • 若进行了分库和分表,原理同上,引擎默认会去遍历所有库的所有表
  • 如果的确有不带参数的需求,需实现Hint分片策略,继承HintShardingAlgorithm实现无参的查询逻辑
  • 二次扩容问题?;;
    • 分表策略上如果类似UC一样可以按区间段分表,那么扩展性强,也无需迁移数据
    • 无法按区间段分表
      • 如果组件未来数据量在可控范围内,那么以预估数据量为前提一次性创建多个表,按普通hash取模来映射,规则简单开发迅速
      • 如果数据量不可控,那么二次扩容一定需要迁移数据,那么在此基础上可考虑一致性hash算法,尽可能缩减需要迁移的数据的范围
  1. // ketama算法:常用来解决一致性hash时hash值范围不一致的问题
  2. public class DemoClass{
  3. public static long hash(String key) {
  4. if (md5 == null) {
  5. try {
  6. md5 = MessageDigest.getInstance("MD5");
  7. } catch (NoSuchAlgorithmException e) {
  8. throw new IllegalStateException("no md5 algorythm found");
  9. }
  10. }
  11. md5.reset();
  12. md5.update(key.getBytes());
  13. byte[] bKey = md5.digest();
  14. long res =
  15. ((long) (bKey[3] & 0xFF) << 24)
  16. | ((long) (bKey[2] & 0xFF) << 16)
  17. | ((long) (bKey[1] & 0xFF) << 8)
  18. | (long) (bKey[0] & 0xFF);
  19. return res & 0xffffffffL;
  20. }
  21. }