分库分表用这个就够了

打印 上一主题 下一主题

主题 940|帖子 940|积分 2820

一、前言

2018年写过一篇分库分表的文章《SpringBoot使用sharding-jdbc分库分表》,但是存在很多不完美的地方比如:

  • sharding-jdbc的版本(1.4.2)过低,现在github上的最新版本都是5.3.2了,很多用法和API都过时了。
  • 分库分表配置采用Java硬编码的方式不够灵活
  • 持久层使用的是spring-boot-starter-data-jpa,而不是主流的mybatis+mybatis-plus+druid-spring-boot-stater
  • 没有支持自定义主键生成策略
二、设计思路

针对上述问题,本人计划开发一个通用的分库分表starter,具备以下特性:

  • 基于ShardingSphere-JDBC版本4.1.1,官方支持的特性我们都支持
  • 支持yaml文件配置,无需编码开箱即用
  • 支持多种数据源,整合主流的mybatis
  • 支持自定义主键生成策略,并提供默认的雪花算法实现
通过查看官方文档,可以发现starter的核心逻辑就是获取分库分表等配置,然后在自动配置类创建数据源注入Spring容器即可。
三、编码实现

3.1 starter工程搭建

首先创建一个spring-boot-starter工程ship-sharding-spring-boot-starter,不会的小伙伴可以参考以前写的教程《【SpringBoot】编写一个自己的Starter》。
创建自动配置类cn.sp.sharding.config.ShardingAutoConfig,并在resources/META-INF/spring.factories文件中配置自动配置类的全路径。
  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.sp.sharding.config.ShardingAutoConfig
复制代码
然后需要在pom.xml文件引入sharding-jbc依赖和工具包guava。
  1.     <properties>
  2.         <java.version>8</java.version>
  3.         <spring-boot.version>2.4.0</spring-boot.version>
  4.         <sharding-jdbc.version>4.1.1</sharding-jdbc.version>
  5.     </properties>
  6.    
  7.       <dependency>
  8.             <groupId>org.apache.shardingsphere</groupId>
  9.             <artifactId>sharding-jdbc-core</artifactId>
  10.             <version>${sharding-jdbc.version}</version>
  11.         </dependency>
  12.         <dependency>
  13.             <groupId>com.google.guava</groupId>
  14.             <artifactId>guava</artifactId>
  15.             <version>18.0</version>
  16.         </dependency>
复制代码
3.2 注入ShardingDataSource

分库分表配置这块,为了方便自定义配置前缀,创建ShardingRuleConfigurationProperties类继承sharding-jbc的YamlShardingRuleConfiguration类即可,代码如下:
  1. /**
  2. * @author Ship
  3. * @version 1.0.0
  4. * @description:
  5. * @date 2023/06/06
  6. */
  7. @ConfigurationProperties(prefix = CommonConstants.COMMON_CONFIG_PREFIX + ".config")
  8. public class ShardingRuleConfigurationProperties extends YamlShardingRuleConfiguration {
  9. }
复制代码
同时sharding-jbc支持自定义一些properties属性,需要单独创建类ConfigMapConfigurationProperties
  1. /**
  2. * @Author: Ship
  3. * @Description:
  4. * @Date: Created in 2023/6/6
  5. */
  6. @ConfigurationProperties(prefix = CommonConstants.COMMON_CONFIG_PREFIX + ".map")
  7. public class ConfigMapConfigurationProperties {
  8.     private Properties props = new Properties();
  9.     public Properties getProps() {
  10.         return props;
  11.     }
  12.     public void setProps(Properties props) {
  13.         this.props = props;
  14.     }
  15. }
复制代码
官方提供了ShardingDataSourceFactory工厂类来创建数据源,但是查看其源码发现createDataSource方法的参数是ShardingRuleConfiguration类,而不是YamlShardingRuleConfiguration
  1. @NoArgsConstructor(access = AccessLevel.PRIVATE)
  2. public final class ShardingDataSourceFactory {
  3.    
  4.     /**
  5.      * Create sharding data source.
  6.      *
  7.      * @param dataSourceMap data source map
  8.      * @param shardingRuleConfig rule configuration for databases and tables sharding
  9.      * @param props properties for data source
  10.      * @return sharding data source
  11.      * @throws SQLException SQL exception
  12.      */
  13.     public static DataSource createDataSource(
  14.             final Map<String, DataSource> dataSourceMap, final ShardingRuleConfiguration shardingRuleConfig, final Properties props) throws SQLException {
  15.         return new ShardingDataSource(dataSourceMap, new ShardingRule(shardingRuleConfig, dataSourceMap.keySet()), props);
  16.     }
  17. }
复制代码
该如何解决配置类参数转换的问题呢?
幸好查找官方文档发现sharding-jdbc提供了YamlSwapper类来实现yaml配置和核心配置的转换
  1. /**
  2. * YAML configuration swapper.
  3. *
  4. * @param <Y> type of YAML configuration
  5. * @param <T> type of swapped object
  6. */
  7. public interface YamlSwapper<Y extends YamlConfiguration, T> {
  8.    
  9.     /**
  10.      * Swap to YAML configuration.
  11.      *
  12.      * @param data data to be swapped
  13.      * @return YAML configuration
  14.      */
  15.     Y swap(T data);
  16.    
  17.     /**
  18.      * Swap from YAML configuration to object.
  19.      *
  20.      * @param yamlConfiguration YAML configuration
  21.      * @return swapped object
  22.      */
  23.     T swap(Y yamlConfiguration);
  24. }
复制代码
ShardingRuleConfigurationYamlSwapper就是YamlSwapper的其中一个实现类。
于是,ShardingAutoConfig的最终代码如下:
  1. package cn.sp.sharding.config;
  2. /**
  3. * @author Ship
  4. * @version 1.0.0
  5. * @description:
  6. * @date 2023/06/06
  7. */
  8. @AutoConfigureBefore(name = CommonConstants.MYBATIS_PLUS_CONFIG_CLASS)
  9. @Configuration
  10. @EnableConfigurationProperties(value = {ShardingRuleConfigurationProperties.class, ConfigMapConfigurationProperties.class})
  11. @Import(DataSourceHealthConfig.class)
  12. public class ShardingAutoConfig implements EnvironmentAware {
  13.     private Map<String, DataSource> dataSourceMap = new HashMap<>();
  14.     @ConditionalOnMissingBean
  15.     @Bean
  16.     public DataSource shardingDataSource(@Autowired ShardingRuleConfigurationProperties configurationProperties,
  17.                                          @Autowired ConfigMapConfigurationProperties configMapConfigurationProperties) throws SQLException {
  18.         ShardingRuleConfigurationYamlSwapper yamlSwapper = new ShardingRuleConfigurationYamlSwapper();
  19.         ShardingRuleConfiguration shardingRuleConfiguration = yamlSwapper.swap(configurationProperties);
  20.         return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfiguration, configMapConfigurationProperties.getProps());
  21.     }
  22.     @Override
  23.     public void setEnvironment(Environment environment) {
  24.         setDataSourceMap(environment);
  25.     }
  26.     private void setDataSourceMap(Environment environment) {
  27.         String names = environment.getProperty(CommonConstants.DATA_SOURCE_CONFIG_PREFIX + ".names");
  28.         for (String name : names.split(",")) {
  29.             try {
  30.                 String propertiesPrefix = CommonConstants.DATA_SOURCE_CONFIG_PREFIX + "." + name;
  31.                 Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, propertiesPrefix, Map.class);
  32.                 // 反射创建数据源
  33.                 DataSource dataSource = DataSourceUtil.getDataSource(dataSourceProps.get("type").toString(), dataSourceProps);
  34.                 dataSourceMap.put(name, dataSource);
  35.             } catch (ReflectiveOperationException e) {
  36.                 e.printStackTrace();
  37.             } catch (Exception e) {
  38.                 e.printStackTrace();
  39.             }
  40.         }
  41.     }
  42. }
复制代码
利用反射创建数据源,就可以解决支持多种数据源的问题。
3.3 自定义主键生成策略

sharding-jdbc提供了UUID和Snowflake两种默认实现,但是自定义主键生成策略更加灵活,方便根据自己的需求调整,接下来介绍如何自定义主键生成策略。
因为我们也是用的雪花算法,所以可以直接用sharding-jdbc提供的雪花算法类,KeyGeneratorFactory负责生成雪花算法实现类的实例,采用双重校验加锁的单例模式。
  1. public final class KeyGeneratorFactory {
  2.     /**
  3.      * 使用shardingsphere提供的雪花算法实现
  4.      */
  5.     private static volatile SnowflakeShardingKeyGenerator keyGenerator = null;
  6.     private KeyGeneratorFactory() {
  7.     }
  8.     /**
  9.      * 单例模式
  10.      *
  11.      * @return
  12.      */
  13.     public static SnowflakeShardingKeyGenerator getInstance() {
  14.         if (keyGenerator == null) {
  15.             synchronized (KeyGeneratorFactory.class) {
  16.                 if (keyGenerator == null) {
  17.                     // 用ip地址当作机器id,机器范围0-1024
  18.                     Long workerId = Long.valueOf(IpUtil.getLocalIpAddress().replace(".", "")) % 1024;
  19.                     keyGenerator = new SnowflakeShardingKeyGenerator();
  20.                     Properties properties = new Properties();
  21.                     properties.setProperty("worker.id", workerId.toString());
  22.                     keyGenerator.setProperties(properties);
  23.                 }
  24.             }
  25.         }
  26.         return keyGenerator;
  27.     }
  28. }
复制代码
雪花算法是由1bit 不用 + 41bit时间戳+10bit工作机器id+12bit序列号组成的,所以为了防止不同节点生成的id重复需要设置机器id,机器id的范围是0-1024,这里是用IP地址转数字取模1024来计算机器id,存在很小概率的重复,也可以用redis来生成机器id(参考雪花算法ID重复问题的解决方案 )。
注意: 雪花算法坑其实挺多的,除了系统时间回溯会导致id重复,单节点并发过高也会导致重复(序列位只有12位代表1ms内最多支持4096个并发)。
查看源码可知自定义主键生成器是通过SPI实现的,实现ShardingKeyGenerator接口即可。
  1. package org.apache.shardingsphere.spi.keygen;
  2. import org.apache.shardingsphere.spi.TypeBasedSPI;
  3. /**
  4. * Key generator.
  5. */
  6. public interface ShardingKeyGenerator extends TypeBasedSPI {
  7.    
  8.     /**
  9.      * Generate key.
  10.      *
  11.      * @return generated key
  12.      */
  13.     Comparable<?> generateKey();
  14. }
复制代码

  • 自定义主键生成器DistributedKeyGenerator
  1. /**
  2. * @Author: Ship
  3. * @Description: 分布式id生成器,雪花算法实现
  4. * @Date: Created in 2023/6/8
  5. */
  6. public class DistributedKeyGenerator implements ShardingKeyGenerator {
  7.     @Override
  8.     public Comparable<?> generateKey() {
  9.         return KeyGeneratorFactory.getInstance().generateKey();
  10.     }
  11.     @Override
  12.     public String getType() {
  13.         return "DISTRIBUTED";
  14.     }
  15.     @Override
  16.     public Properties getProperties() {
  17.         return null;
  18.     }
  19.     @Override
  20.     public void setProperties(Properties properties) {
  21.     }
  22. }
复制代码

  • 创建META-INF/services文件夹,然后在文件夹下创建org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator文件,内容如下:
  1. cn.sp.sharding.key.DistributedKeyGenerator
复制代码

  • yaml文件配置即可
3.4 遗留问题

Spring Boot会在项目启动时执行一条sql语句检查数据源是否可用,因为ShardingDataSource只是对真实数据源进行了封装,没有完全实现Datasouce接口规范,所以会在启动时报错DataSource health check failed,为此需要重写数据源健康检查的逻辑。
创建DataSourceHealthConfig类继承DataSourceHealthContributorAutoConfiguration,然后重写createIndicator方法来重新设置校验sql语句
  1. /**
  2. * @Author: Ship
  3. * @Description:
  4. * @Date: Created in 2023/6/7
  5. */
  6. public class DataSourceHealthConfig extends DataSourceHealthContributorAutoConfiguration {
  7.     private static String validQuery = "SELECT 1";
  8.     public DataSourceHealthConfig(Map<String, DataSource> dataSources, ObjectProvider<DataSourcePoolMetadataProvider> metadataProviders) {
  9.         super(dataSources, metadataProviders);
  10.     }
  11.     @Override
  12.     protected AbstractHealthIndicator createIndicator(DataSource source) {
  13.         DataSourceHealthIndicator healthIndicator = (DataSourceHealthIndicator) super.createIndicator(source);
  14.         if (StringUtils.hasText(validQuery)) {
  15.             healthIndicator.setQuery(validQuery);
  16.         }
  17.         return healthIndicator;
  18.     }
  19. }
复制代码
最后使用@Import注解来注入
  1. @AutoConfigureBefore(name = CommonConstants.MYBATIS_PLUS_CONFIG_CLASS)
  2. @Configuration
  3. @EnableConfigurationProperties(value = {ShardingRuleConfigurationProperties.class, ConfigMapConfigurationProperties.class})
  4. @Import(DataSourceHealthConfig.class)
  5. public class ShardingAutoConfig implements EnvironmentAware {
复制代码
四、测试

假设有个订单表数据量很大了需要分表,为了方便水平扩展,根据订单的创建时间分表,分表规则如下:
  1. t_order_${创建时间所在年}_${创建时间所在季度}
复制代码
订单表结构如下
  1. CREATE TABLE `t_order_2022_3` (
  2.   `id` bigint(20) unsigned NOT NULL COMMENT '主键',
  3.   `order_code` varchar(32) DEFAULT NULL COMMENT '订单号',
  4.   `create_time` bigint(20) NOT NULL COMMENT '创建时间',
  5.   PRIMARY KEY (`id`)
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
复制代码

  • 创建数据库my_springboot,并创建8张订单表t_order_2022_1至t_order_2023_4


  • 创建SpringBoot项目ship-sharding-example,并添加mybatis等相关依赖
  1.   <dependency>
  2.             <groupId>org.mybatis.spring.boot</groupId>
  3.             <artifactId>mybatis-spring-boot-starter</artifactId>
  4.             <version>${mybatis.version}</version>
  5.         </dependency>
  6.         <dependency>
  7.             <groupId>com.baomidou</groupId>
  8.             <artifactId>mybatis-plus-boot-starter</artifactId>
  9.             <version>3.0.1</version>
  10.             <exclusions>
  11.                 <exclusion>
  12.                     <groupId>org.mybatis</groupId>
  13.                     <artifactId>mybatis</artifactId>
  14.                 </exclusion>
  15.             </exclusions>
  16.         </dependency>
  17.         
  18.         <dependency>
  19.             <groupId>com.alibaba</groupId>
  20.             <artifactId>druid-spring-boot-starter</artifactId>
  21.             <version>${druid.version}</version>
  22.         </dependency>
  23.         <dependency>
  24.             <groupId>cn.sp</groupId>
  25.             <artifactId>ship-sharding-spring-boot-starter</artifactId>
  26.             <version>1.0-SNAPSHOT</version>
  27.         </dependency>
  28.         <dependency>
  29.             <groupId>mysql</groupId>
  30.             <artifactId>mysql-connector-java</artifactId>
  31.         </dependency>
复制代码

  • 创建订单实体Order和OrderMapper,代码比较简单省略
  • 自定义分表算法需要实现PreciseShardingAlgorithm和RangeShardingAlgorithm接口的方法,它俩区别如下
接口描述PreciseShardingAlgorithm定义等值查询条件下的分表算法RangeShardingAlgorithm定义范围查询条件下的分表算法创建算法类MyTableShardingAlgorithm
  1. /**
  2. * @Author: Ship
  3. * @Description:
  4. * @Date: Created in 2023/6/8
  5. */
  6. @Slf4j
  7. public class MyTableShardingAlgorithm implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {
  8.     private static final String TABLE_NAME_PREFIX = "t_order_";
  9.     @Override
  10.     public String doSharding(Collection<String> availableTableNames, PreciseShardingValue<Long> preciseShardingValue) {
  11.         Long createTime = preciseShardingValue.getValue();
  12.         if (createTime == null) {
  13.             throw new ShipShardingException("创建时间不能为空!");
  14.         }
  15.         LocalDate localDate = DateUtils.longToLocalDate(createTime);
  16.         final String year = localDate.getYear() + "";
  17.         Integer quarter = DateUtils.getQuarter(localDate);
  18.         for (String tableName : availableTableNames) {
  19.             String dateStr = tableName.replace(TABLE_NAME_PREFIX, "");
  20.             String[] dateArr = dateStr.split("_");
  21.             if (dateArr[0].equals(year) && dateArr[1].equals(quarter.toString())) {
  22.                 return tableName;
  23.             }
  24.         }
  25.         log.error("分表算法对应的表不存在!");
  26.         throw new ShipShardingException("分表算法对应的表不存在!");
  27.     }
  28.     @Override
  29.     public Collection<String> doSharding(Collection<String> availableTableNames, RangeShardingValue<Long> rangeShardingValue) {
  30.         //获取查询条件中范围值
  31.         Range<Long> valueRange = rangeShardingValue.getValueRange();
  32.         // 上限值
  33.         Long upperEndpoint = valueRange.upperEndpoint();
  34.         // 下限值
  35.         Long lowerEndpoint = valueRange.lowerEndpoint();
  36.         List<String> tableNames = Lists.newArrayList();
  37.         for (String tableName : availableTableNames) {
  38.             String dateStr = tableName.replace(MyTableShardingAlgorithm.TABLE_NAME_PREFIX, "");
  39.             String[] dateArr = dateStr.split("_");
  40.             String year = dateArr[0];
  41.             String quarter = dateArr[1];
  42.             Long[] minAndMaxTime = DateUtils.getMinAndMaxTime(year, quarter);
  43.             Long minTime = minAndMaxTime[0];
  44.             Long maxTime = minAndMaxTime[1];
  45.             if (valueRange.hasLowerBound() && valueRange.hasUpperBound()) {
  46.                 // between and
  47.                 if (minTime.compareTo(lowerEndpoint) <= 0 && upperEndpoint.compareTo(maxTime) <= 0) {
  48.                     tableNames.add(tableName);
  49.                 }
  50.             } else if (valueRange.hasLowerBound() && !valueRange.hasUpperBound()) {
  51.                 if (maxTime.compareTo(lowerEndpoint) > 0) {
  52.                     tableNames.add(tableName);
  53.                 }
  54.             } else {
  55.                 if (upperEndpoint.compareTo(minTime) > 0) {
  56.                     tableNames.add(tableName);
  57.                 }
  58.             }
  59.         }
  60.         if (tableNames.size() == 0) {
  61.             log.error("分表算法对应的表不存在!");
  62.             throw new ShipShardingException("分表算法对应的表不存在!");
  63.         }
  64.         return tableNames;
  65.     }
  66. }
复制代码

  • 在application.yaml上添加数据库配置和分表配置
  1. spring:
  2.   application:
  3.     name: ship-sharding-example
  4. mybatis-plus:
  5.   base-package: cn.sp.sharding.dao
  6.   mapper-locations: classpath*:/mapper/*Mapper.xml
  7.   configuration:
  8.     #开启自动驼峰命名规则(camel case)映射
  9.     map-underscore-to-camel-case: true
  10.     #延迟加载,需要和lazy-loading-enabled一起使用
  11.     aggressive-lazy-loading: true
  12.     lazy-loading-enabled: true
  13.     #关闭一级缓存
  14.     local-cache-scope: statement
  15.     #关闭二级级缓存
  16.     cache-enabled: false
  17. ship:
  18.   sharding:
  19.     jdbc:
  20.       datasource:
  21.         names: ds0
  22.         ds0:
  23.           driver-class-name: com.mysql.cj.jdbc.Driver
  24.           type: com.alibaba.druid.pool.DruidDataSource
  25.           url: jdbc:mysql://127.0.0.1:3306/my_springboot?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
  26.           username: root
  27.           password: 1234
  28.           initial-size: 5
  29.           minIdle: 5
  30.           maxActive: 20
  31.           maxWait: 60000
  32.           timeBetweenEvictionRunsMillis: 60000
  33.           minEvictableIdleTimeMillis: 300000
  34.           validationQuery: SELECT 1 FROM DUAL
  35.           testWhileIdle: true
  36.           testOnBorrow: false
  37.           testOnReturn: false
  38.           poolPreparedStatements: true
  39.           maxPoolPreparedStatementPerConnectionSize: 20
  40.           useGlobalDataSourceStat: true
  41.           connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=2000;druid.mysql.usePingMethod=false
  42.       config:
  43.         binding-tables: t_order
  44.         tables:
  45.           t_order:
  46.             actual-data-nodes: ds0.t_order_${2022..2023}_${1..4}
  47.             # 配置主键生成策略
  48.             key-generator:
  49.               type: DISTRIBUTED
  50.               column: id
  51.             table-strategy:
  52.               standard:
  53.                 sharding-column: create_time
  54.                 # 配置分表算法
  55.                 precise-algorithm-class-name: cn.sp.sharding.algorithm.MyTableShardingAlgorithm
  56.                 range-algorithm-class-name: cn.sp.sharding.algorithm.MyTableShardingAlgorithm
复制代码

  • 现在可以进行测试了,首先写一个单元测试测试数据插入情况。
  1. @Test
  2.     public void testInsert() {
  3.         Order order = new Order();
  4.         order.setOrderCode("OC001");
  5.         order.setCreateTime(System.currentTimeMillis());
  6.         orderMapper.insert(order);
  7.     }
复制代码
运行testInsert()方法,打开t_order_2023_2表发现已经有了一条订单数据

并且该数据的create_time是1686383781371,转换为时间为2023-06-10 15:56:21,刚好对应2023年第二季度,说明数据正确的路由到了对应的表里。
然后测试下数据查询情况
  1. @Test
  2.     public void testQuery(){
  3.         QueryWrapper<Order> wrapper = new QueryWrapper<>();
  4.         wrapper.lambda().eq(Order::getOrderCode,"OC001");
  5.         List<Order> orders = orderMapper.selectList(wrapper);
  6.         System.out.println(JSONUtil.toJsonStr(orders));
  7.     }
复制代码
运行testQuery()方法后可以在控制台看到输出了订单报文,说明查询也没问题。
  1. [{"id":1667440550397132802,"orderCode":"OC001","createTime":1686383781371}]
复制代码
五、总结


本文代码已经上传到github,后续会把ship-sharding-spring-boot-starter上传到maven中央仓库方便使用,如果觉得对你有用的话希望可以点个赞让更多人看到
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

美食家大橙子

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表