Spring Boot 2.2.7+Mybatis Plus+Druid+shardingsphere 整合

目录

  • Spring Boot 2.2.7+Mybatis Plus+Druid+shardingsphere 整合
    • 相关网站
    • 任务说明
    • SQL举例
    • 依赖坐标
    • YML配置
    • Java关键代码
    • 运行
    • 其他

Spring Boot 2.2.7+Mybatis Plus+Druid+shardingsphere 整合

没啥好说的,领导叫我水平分表,将业务订单数据水平分表,减少一个表中的数据量,加快数据查询。
除了shardingsphere外,其他3项都是项目中已经整合了的技术。写这篇文章主要是为了记录shardingsphere整合其他3项技术的过程。
shardingsphere定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。使用上来说,在你的客户端通过解析你的sql来判断你访问的某个表x是不是已经分成了形如x_1、x_2、x_3这种命名规则的表。

相关网站

shardingsphere
Druid

任务说明

这里只列出关键信息:

  1. 需要拆分的表原名为task_info,拆分后的表名为task_info_202001、task_info_202002、task_info_202003……task_info_202012。可见,分开后的表分别保存原表不同月份的数据。
  2. task_info有对应的订单结果表task_result,同样需要按月拆分。task_result中task_id_关联task_info的id_,但我没在数据库里做外键关联。
  3. 为了将这几张表在逻辑上认作一张表,那它们之间必然有相同列。可以是单列,比如说主键那一列。也可以是多列,具体看需求。其他列可相同可不同,但是为了一致性,最好定为一样的。
  4. 分片的那一列我定为主键id,注解有32位。其中前8位是随机的,中间18位取用shardingsphere的SnowflakeShardingKeyGenerator算出来的值,最后6位为年月,如“202005”,可以通过最后这6位来定位是哪张表。

SQL举例

CREATE TABLE `task_info_202001` (
  `id_` char(32) NOT NULL COMMENT '无业务意义主键',
  `content_` json NOT NULL COMMENT '订单内容',
  `create_date_` datetime(6) DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
  `update_date_` datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
  PRIMARY KEY (`id_`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='任务表';

CREATE TABLE `task_result_202001` (
  `id_` char(32) NOT NULL,
  `task_id_` char(32) DEFAULT NULL COMMENT '任务ID',
  `error_code_` varchar(8) DEFAULT NULL COMMENT '业务错误码',
  `message_` text COMMENT '响应消息',
  `result_` json DEFAULT NULL COMMENT '业务处理结果(数据)',
  `create_date_` datetime(6) DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
  PRIMARY KEY (`id_`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='任务处理结果表';

其他月份更改表名即可。

依赖坐标

springboot 选2.2.7的版本,2.3.0还存在一些问题。

<parent>
   <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.2.7.RELEASEversion>
    <relativePath/> 
parent>

其他的一些选择:

 <dependency>
    <groupId>org.mybatis.spring.bootgroupId>
    <artifactId>mybatis-spring-boot-starterartifactId>
    <version>2.1.2version>
dependency>

<dependency>
   <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-boot-starterartifactId>
    <version>3.3.1version>
dependency>


<dependency>
    <groupId>org.apache.shardingspheregroupId>
    <artifactId>sharding-jdbc-spring-boot-starterartifactId>
    <version>4.1.0version>
dependency>



<dependency>
     <groupId>com.alibabagroupId>
     <artifactId>druid-spring-boot-starterartifactId>
     <version>1.1.22version>
 dependency>

YML配置

spring:
  shardingsphere:
    datasource:
      names: ds0
      ds0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://****
        username: *******
        password: *******
        filters: stat
        maxActive: 20
        initialSize: 1
        maxWait: 1000
        minIdle: 1
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: select 1 from dual
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        maxOpenPreparedStatements: 20
    sharding:
      tables:
        task_info:
          #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
          actual-data-nodes: ds0.task_info_$->{2020}$->{['01','02','03','04','05','06','07','08','09','10','11','12']}
          #用于单分片键的标准分片场景
          table-strategy:
            complex:
              sharding-columns: id_,create_date_
              #复合分片算法类名称。该类需实现ComplexKeysShardingAlgorithm接口并提供无参数的构造器
              algorithmClassName: com.example.privatebatistest.config.MonthTableComplexShardingAlgorithm
        task_result:
          actual-data-nodes: ds0.task_result_$->{2020}$->{['01','02','03','04','05','06','07','08','09','10','11','12']}
          table-strategy:
            standard:
              sharding-column: task_id_
              precise-algorithm-class-name: com.example.mybatistest.config.MonthTableShardingAlgorithm
      binding-tables:
        - task_info,task_result
      default-data-source-name: ds0
      defaultTableStrategy: #默认表分片策略,同分库策略
        none:
    props:
      sql:
        show: true
      executor:
        size: 8
server:
  port: 10489
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    cache-enabled: true
logging:
  level:
    root: info
    ShardingSphere-SQL: debug

这里注意几点问题:

  1. names: ds0必须要配,多个数据源用逗号分隔。
  2. binding-tables的配置是为了同步task_info和task_result的变化,即限制如task_info_202001 left join task_result_202001同月份关联查询,而不出现如task_info_202001 left join task_result_202002这种没有意义的关联查询。
  3. table-strategy只能5选1,不能混合多种配置。否则初始化报错。

关于这些参数都有什么意义,可以参考这里

我将数据分片部分摘抄下来,防止这个网址失效:

dataSources: #数据源配置,可配置多个data_source_name
  : # `!!`表示实例化该类
    driverClassName: #数据库驱动类名
    url: #数据库url连接
    username: #数据库用户名
    password: #数据库密码
    # ... 数据库连接池的其它属性

shardingRule:
  tables: #数据分片规则配置,可配置多个logic_table_name
    : #逻辑表名称
      actualDataNodes: #由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况
        
      databaseStrategy: #分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一
        standard: #用于单分片键的标准分片场景
          shardingColumn: #分片列名称
          preciseAlgorithmClassName: #精确分片算法类名称,用于=和IN。。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器
          rangeAlgorithmClassName: #范围分片算法类名称,用于BETWEEN,可选。。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器
        complex: #用于多分片键的复合分片场景
          shardingColumns: #分片列名称,多个列以逗号分隔
          algorithmClassName: #复合分片算法类名称。该类需实现ComplexKeysShardingAlgorithm接口并提供无参数的构造器
        inline: #行表达式分片策略
          shardingColumn: #分片列名称
          algorithmInlineExpression: #分片算法行表达式,需符合groovy语法
        hint: #Hint分片策略
          algorithmClassName: #Hint分片算法类名称。该类需实现HintShardingAlgorithm接口并提供无参数的构造器
        none: #不分片
      tableStrategy: #分表策略,同分库策略
      keyGenerator: 
        column: #自增列名称,缺省表示不使用自增主键生成器
        type: #自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
        props: #属性配置, 注意:使用SNOWFLAKE算法,需要配置worker.id与max.tolerate.time.difference.milliseconds属性。若使用此算法生成值作分片值,建议配置max.vibration.offset属性
          : 属性名称
      
  bindingTables: #绑定表规则列表
  - , logic_table_name2, ...> 
  - , logic_table_name4, ...>
  - , logic_table_name_y, ...>
  broadcastTables: #广播表规则列表
  - table_name1
  - table_name2
  - table_name_x
  
  defaultDataSourceName: #未配置分片规则的表将通过默认数据源定位  
  defaultDatabaseStrategy: #默认数据库分片策略,同分库策略
  defaultTableStrategy: #默认表分片策略,同分库策略
  defaultKeyGenerator: #默认的主键生成算法 如果没有设置,默认为SNOWFLAKE算法
    type: #默认自增列值生成器类型,缺省将使用org.apache.shardingsphere.core.keygen.generator.impl.SnowflakeKeyGenerator。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID
    props:
      : #自增列值生成器属性配置, 比如SNOWFLAKE算法的worker.id与max.tolerate.time.difference.milliseconds

  masterSlaveRules: #读写分离规则,详见读写分离部分
    : #数据源名称,需要与真实数据源匹配,可配置多个data_source_name
      masterDataSourceName: #详见读写分离部分
      slaveDataSourceNames: #详见读写分离部分
      loadBalanceAlgorithmType: #详见读写分离部分
      props: #读写分离负载算法的属性配置
        : #属性值
      
props: #属性配置
  sql.show: #是否开启SQL显示,默认值: false
  executor.size: #工作线程数量,默认值: CPU核数
  max.connections.size.per.query: # 每个查询可以打开的最大连接数量,默认为1
  check.table.metadata.enabled: #是否在启动时检查分表元数据一致性,默认值: false

Java关键代码

  1. 首先在@SpringBootApplication存在的主类上配置

    @SpringBootApplication(exclude = {
            DruidDataSourceAutoConfigure.class,
            DataSourceAutoConfiguration.class,
    })
    @MapperScan("com.example.mybatistest.mapper")
    public class MybatistestApplication {
        public static void main(String[] args) {
            SpringApplication.run(MybatistestApplication.class, args);
        }
    }
    

    解释一下, 排除DruidDataSourceAutoConfigure.class是为了防止出现如下错误:

    ***************************
    APPLICATION FAILED TO START
    ***************************
    
    Description:
    
    Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
    
    Reason: Failed to determine a suitable driver class
    
    
    Action:
    
    Consider the following:
    	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
    	If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
    

    因为DruidDataSourceAutoConfigure默认用了SpringBoot顶级的配置,如下:

    spring:
       datasource: 
         url: 
    

    如果不存在,Druid会启动不成功。我们需要排除它。不过别担心,shardingsphere会帮助我们完成Druid的生成,在前面的配置中可见。
    另外一个MapperScan是用来扫描Mybatis的mapper,这是MyBatis的常规玩法。

  2. 配置中我们用到了一个算法,用来对task_result精确分片:

    import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
    import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
    
    import java.util.Collection;
    
    public class MonthTableShardingAlgorithm implements PreciseShardingAlgorithm<String> 			 	{
       @Override
       public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
           // 根据配置的分表规则生成目标表的后缀
           String tableExt = shardingValue.getValue().substring(26);
           for (String availableTableName : availableTargetNames) {
               if (availableTableName.endsWith(tableExt)) {
               	// 匹配成功返回正确表名
                   return availableTableName;
               }
           }
           return null;
       }
    }
    

    以上代码利用查询时的shardingColumn的取值,判断正确的表名,返回给上层做决策。

  3. 另外一个复杂点的复合分片算法,是为了更好地处理 task_info 表的 id_create_date_ 字段:

    import com.google.common.collect.Range;
    import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm;
    import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue;
    import org.springframework.util.ObjectUtils;
    
    import java.sql.Timestamp;
    import java.time.LocalDate;
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.Collection;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    
    public class MonthTableComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm<Comparable<?>> {
        private final static DateTimeFormatter createDateDf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        private final static DateTimeFormatter targetNamesDateDf = DateTimeFormatter.ofPattern("yyyyMMdd");
    
        @Override
        public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Comparable<?>> shardingValue) {
            Set<String> finalTargetNames = new HashSet<>();
            //拿到逻辑表名的长度,比如task_info的长度,加上1个下划线的长度,方便后面获取最后的年月
            int tablePrefixLength = shardingValue.getLogicTableName().length() + 1;
            if (shardingValue.getColumnNameAndShardingValuesMap().size() > 0) {
                /**
                 * 以下是对精确分片的处理,比如= 或者 in
                 */
                Map<String, Collection<Comparable<?>>> shardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
                for (String availableTargetName : availableTargetNames) {
                    LocalDate targetNamesDt = getLocalDateFromTableName(tablePrefixLength, availableTargetName);
                    if (!ObjectUtils.isEmpty(shardingValuesMap.get("create_date_"))) {
                        if (shardingValuesMap.get("create_date_").size() > 0) {
                            Collection<Comparable<?>> createDateStr = shardingValuesMap.get("create_date_");
                            for (Comparable<?> date :
                                    createDateStr) {
                                if (date instanceof Timestamp) {
                                    LocalDate timestamp = ((Timestamp) date).toLocalDateTime().toLocalDate();
                                    if (isSameMonth(timestamp, targetNamesDt)) {
                                        finalTargetNames.add(availableTargetName);
                                    }
                                }
                            }
                        }
                    } else if (!ObjectUtils.isEmpty(shardingValuesMap.get("id_"))) {
                        if (shardingValuesMap.get("id_").size() > 0) {
                            Collection<Comparable<?>> idStr = shardingValuesMap.get("id_");
                            for (Comparable<?> id :
                                    idStr) {
                                if (id instanceof String) {
                                    String tableExt = ((String) id).substring(26);
                                    if (availableTargetName.endsWith(tableExt)) {
                                        finalTargetNames.add(availableTargetName);
                                    }
                                }
                            }
                        }
                    }
                }
            } else if (shardingValue.getColumnNameAndRangeValuesMap().size() > 0) {
                /**
                 * 以下是对范围分片的处理,比如>、<等
                 * 这里主要处理create_date_,通过create_date_来确认是那些表
                 */
                Map<String, Range<Comparable<?>>> rangeValuesMap = shardingValue.getColumnNameAndRangeValuesMap();
                Range<Comparable<?>> createDate = rangeValuesMap.get("create_date_");
                //2020-05-19 00:00:00
                LocalDate lowerEndpointDt = null, upperEndpointDt = null;
                if (createDate.hasLowerBound()) {
                    lowerEndpointDt = LocalDateTime.parse((String) createDate.lowerEndpoint(), createDateDf).toLocalDate();
                }
                if (createDate.hasUpperBound()) {
                    upperEndpointDt = LocalDateTime.parse((String) createDate.upperEndpoint(), createDateDf).toLocalDate();
                }
                Set<String> lowerSet = new HashSet<>();
                Set<String> upperSet = new HashSet<>();
                for (String availableTargetName : availableTargetNames) {
                    //例如202002
                    LocalDate targetNamesDt = getLocalDateFromTableName(tablePrefixLength, availableTargetName);
    
                    if (lowerEndpointDt != null &&
                            (targetNamesDt.isAfter(lowerEndpointDt) ||
                                    isSameMonth(lowerEndpointDt, targetNamesDt)
                            )
                    ) {
                        lowerSet.add(availableTargetName);
                    }
                    if (upperEndpointDt != null &&
                            (targetNamesDt.isBefore(upperEndpointDt) ||
                                    (isSameMonth(upperEndpointDt, targetNamesDt))
                            )
                    ) {
                        upperSet.add(availableTargetName);
                    }
                }
                lowerSet.retainAll(upperSet);
                finalTargetNames = lowerSet;
            }
            return finalTargetNames;
        }
    
        /**
         * 是否是同一个月
         * @param date 待比较日期
         * @param targetDate 目标日期
         * @return 真为是同一个月
         */
        private boolean isSameMonth(LocalDate date, LocalDate targetDate) {
            return targetDate.getYear() == date.getYear() && targetDate.getMonthValue() == date.getMonthValue();
        }
    
        /**
         *
         * @param tablePrefixLength 表名前面相同部分的前缀长度
         * @param availableTargetName 可用表名
         * @return
         */
        private LocalDate getLocalDateFromTableName(int tablePrefixLength, String availableTargetName) {
            String targetNamesDateStr = availableTargetName.substring(tablePrefixLength);
            return LocalDate.parse(targetNamesDateStr + "01", targetNamesDateDf);
        }
    }
    
  4. Id生成算法如下,主要为给新增数据添加ID:

    import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
    import org.apache.shardingsphere.core.strategy.keygen.SnowflakeShardingKeyGenerator;
    import org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator;
    
    import java.util.Date;
    import java.util.Properties;
    import java.util.Random;
    
    public final class ShardingIdGenerator implements ShardingKeyGenerator, IdentifierGenerator {
    
        private final static SnowflakeShardingKeyGenerator snowflakeShardingKeyGenerator;
    
        static {
            snowflakeShardingKeyGenerator = new SnowflakeShardingKeyGenerator();
        }
    
        private static long getRandom(long n) {
            long min = 1, max = 9;
            for (int i = 1; i < n; i++) {
                min *= 10;
                max *= 10;
            }
            return (((long) (new Random().nextDouble() * (max - min)))) + min;
        }
    
        @Override
        public Comparable<?> generateKey() {
            Comparable<?> key = snowflakeShardingKeyGenerator.generateKey();
            String date = DateFormatUtil.format(new Date(), "yyyyMM");
            return String.valueOf(getRandom(8)) + key + date;
        }
    
        @Override
        public String getType() {
            return "DATEFLAKE";
        }
    
        @Override
        public Properties getProperties() {
            return snowflakeShardingKeyGenerator.getProperties();
        }
    
        @Override
        public void setProperties(Properties properties) {
            snowflakeShardingKeyGenerator.setProperties(properties);
        }
    
        @Override
        public Number nextId(Object entity) {
            return 1;
        }
    
        @Override
        public String nextUUID(Object entity) {
            return (String) generateKey();
        }
    }
    
  5. TaskInfo:

    import com.baomidou.mybatisplus.annotation.*;
    import lombok.Data;
    
    @Data
    @TableName("task_info")
    public class TaskInfo {
    
        /**
         * 表ID
         */
        @TableId(value = "id_", type = IdType.ASSIGN_UUID)
        private String id;
        
        ……
    }
    
  6. TaskResult:

    import com.baomidou.mybatisplus.annotation.*;
    import lombok.Data;
    	
    @Data
    @TableName("task_result")
    public class TaskResult {
    
        @TableId(value = "id_", type = IdType.ASSIGN_UUID)
        private String id;
        
        @TableField("task_id_")
        private String taskId;
    	    
    	    ……
    }
    
  7. 其他代码如Mapper、Service、Controller等可以自己实现,这些东西和平常使用上不会有变化。

    public interface TaskInfoMapper extends BaseMapper<TaskInfo> {
    
        /**
         * 

    * 查询 : 根据state状态查询用户列表,分页显示 *

    * * @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位(你可以继承Page实现自己的分页对象) * @return 分页对象 */
    @Select("select i.id_ as id, " + " i.content_ as content " + " from task_info i " + " left join task_result r on i.id_ = r.task_id_ ${ew.customSqlSegment} " + " order by i.create_date_ desc ") Page<Map<String, Object>> findTaskByCondition(@Param(Constants.WRAPPER) Wrapper<Map<String, Object>> wrapper, Page<?> page); @Insert("insert into task_info (content_) values (#{content})") int insertX(TaskInfo taskInfo); }

    可以看到 task_infotask_result 并不需要我们刻意加上_202005这样的后缀。在org.apache.shardingsphere.sharding.route.engine.ShardingRouteDecorator#decorate方法中有详细的从逻辑表到真实表路由规则。可以对源码位置进行调试即可理清一些逻辑。

  8. MyBatis需要分页插件,已经自己写的IdentifierGenerator生成数据库ID:

    @EnableTransactionManagement
    @Configuration
    @MapperScan("com.example.privatebatistest.mapper")
    public class MybatisPlusConfig {
      /**
         * 将shardingsphere的id生成器拿过来用
         * 如果不想这样子
         * 请使用SPI独立注入到shardingsphere
         * @return
         */
        @Bean
        public IdentifierGenerator idGenerator() {
            return new ShardingIdGenerator();
        }
    
        @Bean
        public PaginationInterceptor paginationInterceptor() {
            PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
            // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
            paginationInterceptor.setOverflow(false);
            // 设置最大单页限制数量,默认 500 条,-1 不受限制
            paginationInterceptor.setLimit(500);
            // 开启 count 的 join 优化,只针对部分 left join
            paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
            return paginationInterceptor;
        }
    }
    
  9. 如果想让ShardingIdGenerator只在shardingsphere中使用,那么需要按官方提供的 Service Provider Interface (SPI)1 去完成配置操作。
    操作为:在resources目录下创建META-INF目录,接着在META-INF目录下创建services目录,然后在services目录中创建名为org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator文件,内容为com.example.privatebatistest.util.ShardingIdGenerator。这个内容指向ShardingIdGenerator完整类路径。配合配置文件完成注入到shardingsphere中的任务。
    如图所示: Spring Boot 2.2.7+Mybatis Plus+Druid+shardingsphere 整合_第1张图片

以上代码虽然挺多的,但是有几个是MyBatis需要的类,ShardingSphere的主要功能都在配置中完成。

运行

在控制台会有如下输出:
Spring Boot 2.2.7+Mybatis Plus+Druid+shardingsphere 整合_第2张图片
那么配置就完成了。

其他

草草写完,还有待完善。如遇到问题请各位大神锤锤小弟我,不吝赐教,感谢。


  1. https://shardingsphere.apache.org/document/legacy/4.x/document/cn/features/spi/ ↩︎

你可能感兴趣的:(后端,java,mybatis,mysql)