SpringBoot 集成 Sharding-JDBC 实现读写分离、分库分表、自定义复杂分片策略

文章目录

一、Sharding-JDBC的应用场景

      Sharding-JDBC是针对分库分表后的操作简化,相当于增强版的JDBC驱动,常被用于实现应用层读写分离以及分库分表,具体概括可以查看官网这里不做过多说明。
      官方文档

二、SpringBoot 集成 Sharding-JDBC

2.1、前期准备

      要想比较好的体验Sharding-JDBC能力最好使用一主多从数据库模式,我这里会使用MySQL一主两从,如果嫌比较麻烦也可以直接使用一主也行,区别不是很大,再执行日志中都能看出区别。
      想要部署MySQL一主两从可以参考:Linux从零部署MySQL8.0 主从复制(一主两从)

  • 创建一个测试库sharding-jdbc-test然后新增一张测试表后续测试都会使用
CREATE TABLE `t_order` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_id` bigint DEFAULT NULL COMMENT '订单id(通过雪花算法生成)',
  `order_no` varchar(100) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `goods_info` varchar(100) DEFAULT NULL COMMENT '商品信息',
  `to_address` varchar(100) DEFAULT NULL COMMENT '收件地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
);
  • 代码结构(这里会使用到Mybatis-plus方便进行测试)
    在这里插入图片描述

2.2、导入pom.xml依赖包

我这里使用SpringBoot2.x sharding-jdbc4.x 不同大版本可能会有冲突,如果测试时发现有冲突先检测一下代码是否有问题,如果代码没有问题可以调整一下包的版本。

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
    </parent>

    <properties>
        <hutool.version>5.2.5</hutool.version>
        <fastjson.version>1.2.74</fastjson.version>
        <lombok.version>1.18.12</lombok.version>
        <common.version>1.0-SNAPSHOT</common.version>
        <commons-lang3.version>3.10</commons-lang3.version>

        <druid.version>1.1.10</druid.version>
        <mybatis-plus.version>3.5.1</mybatis-plus.version>
        <sharding-jdbc.version>4.0.0</sharding-jdbc.version>
    </properties>

    <dependencies>
        <!--工具包-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>

        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons-lang3.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--Mysql依赖包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <!--druid数据源驱动 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>${sharding-jdbc.version}</version>
        </dependency>
    </dependencies>

2.3、结构代码实现

2.3.1、MybatisPlusConfig(分页插件)
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}
2.3.2、TOrder(订单对象)
@Data
public class TOrder implements Serializable {
    private static final long serialVersionUID = 1L;

    /** 订单id */
    @TableId(value = "id",type = IdType.AUTO)
    private Long id;

    /** 订单id (通过雪花算法生成)*/
    private Long orderId;

    /** 订单编号 */
    private String orderNo;

    /** 用户id */
    private Integer userId;

    /** 商品信息 */
    private String goodsInfo;

    /** 收件地址 */
    private String toAddress;

    /** 创建时间 */
    private Date createTime;
}
2.3.3、TOrderMapper(订单mapper接口)
public interface TOrderMapper extends BaseMapper<TOrder> {
    @Select("SELECT t1.*,t2.`name` FROM t_order t1 LEFT JOIN t_user t2 ON t1.user_id = t2.id;")
    List<Map<String,Object>> queryOrderList();
}
  • ITOrderService(订单接口)
public interface ITOrderService extends IService<TOrder> {
	void updateOrder(Long id, String toAddress);
}
2.3.4、TOrderServiceImpl(订单接口实现)
@Service
public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements ITOrderService {
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateOrder(Long id, String toAddress){
        // 强制路由主库
        // HintManager.getInstance().setMasterRouteOnly();
        TOrder order = this.getById(id);
        if(order == null){
        	System.out.println("订单不存在");
            return;
        }
        this.lambdaUpdate().eq(TOrder::getId, id).set(TOrder::getToAddress, toAddress).update();

        order = this.getById(id);
    }
}
2.3.5、ShardingApplication(启动类)
@SpringBootApplication(exclude = {DruidDataSourceAutoConfigure.class})
@MapperScan("com.kerwin.service.mapper")
public class ShardingApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShardingApplication.class);
    }
}
2.3.6、application.yml(这个配置文件中只配置公用信息,具体分库分表信息通过active指定)
server:
  port: 9090

spring:
  profiles:
    active: duxiefenli
#    active: fenbiao-qumo
#    active: duxiefenli-fenbiao-qumo
#    active: fenkufenbiao-qumo
#    active: zidingyi-fenbiao-date
  main:
    allow-bean-definition-overriding: true

logging:
  level:
    com.baomidou: debug
    com.kerwin: debug

mybatis-plus:
  configuration:
    #开启详细日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # Mybatis 一级缓存,默认为 SESSION 开启一级缓存,STATEMENT 关闭一级缓存
    # SESSION session 级别缓存,同一个 session 相同查询语句不会再次查询数据库
    localCacheScope: STATEMENT
    # Mybatis 二级缓存,默认为 true
    cacheEnabled: false
2.3.7、SpringBoot测试类(用于测试所有应用场景,内容后面补充)
@RunWith(SpringRunner.class)
@SpringBootTest
public class ShardingJDBCTest {
    @Autowired
    private ITOrderService orderService;

    @Autowired
    private TOrderMapper orderMapper;
}

三、Sharding-JDBC常用场景

      Sharding-JDBC有多种使用场景,可以通过配置文件灵活配置读写分离、分库分表等,这里会对一下常用方式做配置说明。

3.1、读写分离

3.1.1、准备读写分离配置文件 application-duxiefenli.yml
# 分表配置
spring:
  shardingsphere:
    datasource:
      # 全部数据源名称 多个用逗号隔开
      names:
        master1,slave1,slave2
      # 主数据源
      master1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://172.16.8.181:3306/sharding-jdbc-test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
        validation-query: SELECT 1  #验证数据库服务可用性的查询SQL,一般设置为SELECT 1
        initial-size: 2 #初始连接 默认0
        max-active: 10 #最大连接数 默认8
        min-idle: 2 # 最小连接数 默认0
        max-wait: 5000 # 获取连接最大等待时间,单位毫秒 默认-1一直等待
      # 从数据源
      slave1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://172.16.8.191:3306/sharding-jdbc-test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
        validation-query: SELECT 1  #验证数据库服务可用性的查询SQL,一般设置为SELECT 1
        initial-size: 2 #初始连接 默认0
        max-active: 10 #最大连接数 默认8
        min-idle: 2 # 最小连接数 默认0
        max-wait: 5000 # 获取连接最大等待时间,单位毫秒 默认-1一直等待
      slave2:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://172.16.8.192:3306/sharding-jdbc-test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456

    masterslave:
      # 读写分离配置 用于配置从库负载均衡算法类型,可选值:ROUND_ROBIN(轮询),RANDOM(随机)
      load-balance-algorithm-type: round_robin
      # 最终的数据源名称
      name: dataSource
      # 主库数据源名称
      master-data-source-name: master1
      # 从库数据源名称列表,多个逗号分隔
      slave-data-source-names: slave1,slave2
    props:
      # 开启SQL显示,默认false
      sql:
        show: true
3.1.2、测试代码
    //----------------------------- 读写分离测试 start -----------------------------
    @Test
    public void duxiefenliTest(){
        // 1、测试自动主库写操作(在执行写操作时会自动选择主库)
        for (int i = 0; i < 10; i++) {
            TOrder tOrder = new TOrder();
            tOrder.setOrderNo("NO"+RandomUtil.randomNumbers(10));
            tOrder.setUserId(RandomUtil.randomInt(10));
            tOrder.setGoodsInfo("商品"+RandomUtil.randomString(5));
            tOrder.setToAddress("地址"+RandomUtil.randomString(5));
            tOrder.setCreateTime(new Date());
            orderService.save(tOrder);
        }
        // 2、测试从库自动轮询读操作
        TOrder tOrder1 = orderService.getById(1);
        TOrder tOrder2 = orderService.getById(2);
    }
    //----------------------------- 读写分离测试 end -----------------------------
3.1.3、测试读写分离效果

      这里直接运行测试类中的读写分离测试方法,运行之后拉到最下面可以看到插入数据时使用的数据源是DataSources: master1,而查询时使用的数据源是DataSources: slave1 DataSources: slave2,多次查看可以看到会对两个从数据源进行轮询。

在这里插入图片描述

3.2、读写分离强制路由主库

3.2.1、准备读写分离配置文件 application-duxiefenli.yml

      配置文件和读写分离配置文件一致

3.2.2、测试代码
    //----------------------------- 读写分离强制路由主库测试 start -----------------------------
    @Test
    public void duxiefenliMasterRouteTest(){
        // 测试一个事务中有写也有读操作数据源路由情况
        orderService.updateOrder(1L,"罗马");
    }
    //----------------------------- 读写分离强制路由主库测试 end -----------------------------
3.1.3、测试未开启强制路由主库同一个事务中 查询->更新->查询

在这里插入图片描述
这里可以看到第一次查询路由到了从库,第二次查询路由到了主库,在同一个事务中如果进行了修改操作那么后面都会路由主库。
在这里插入图片描述

3.1.4、测试开启强制路由主库同一个事务中 查询->更新->查询

打开强制路由主库HintManager.getInstance().setMasterRouteOnly();运行后可以看到第一次查询也是使用的主库

在这里插入图片描述

3.3、分表(根据user_id取模分表)

3.3.1、准备分片表

       这里准备两个分片表,t_order_0 t_order_1

CREATE TABLE `t_order_0` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_id` bigint DEFAULT NULL COMMENT '订单id(通过雪花算法生成)',
  `order_no` varchar(100) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `goods_info` varchar(100) DEFAULT NULL COMMENT '商品信息',
  `to_address` varchar(100) DEFAULT NULL COMMENT '收件地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
);
CREATE TABLE `t_order_1` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_id` bigint DEFAULT NULL COMMENT '订单id(通过雪花算法生成)',
  `order_no` varchar(100) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `goods_info` varchar(100) DEFAULT NULL COMMENT '商品信息',
  `to_address` varchar(100) DEFAULT NULL COMMENT '收件地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
);
3.3.2、准备分表配置文件 application-fenbiao-qumo.yml
# 取模分表配置
spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource:
      # 全部数据源名称 多个用逗号隔开
      names:
        master1
      # 主数据源
      master1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://172.16.8.181:3306/sharding-jdbc-test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
    props:
      # 开启SQL显示,默认false
      sql:
        show: true
    # 分表配置
    sharding:
      tables:
        t_order:
          # 由数据源名.表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式
          # ds0代表数据源名称,t_order表名称 ,$->{0..1} inline表达式代表取值0 1
          actual-data-nodes: master1.t_order_$->{0..1}
          table-strategy:
            # 分表策略为inline
            inline:
              # 分表列名
              sharding-column: user_id
              # 分表算法 根据t_order表中的user_id取模2进行数据分片存储
              algorithm-expression: t_order_$->{user_id % 2}
3.3.3、测试代码
    //----------------------------- 分表测试 start -----------------------------
    @Test
    public void fenbiaoTest(){
        // 1、测试自动主库写操作(在执行写操作时会自动选择主库)
        for (int i = 0; i < 10; i++) {
            TOrder tOrder = new TOrder();
            tOrder.setOrderNo("NO"+RandomUtil.randomNumbers(10));
            tOrder.setUserId(RandomUtil.randomInt(10));
            tOrder.setGoodsInfo("商品"+RandomUtil.randomString(5));
            tOrder.setToAddress("地址"+RandomUtil.randomString(5));
            tOrder.setCreateTime(new Date());
            orderService.save(tOrder);
        }
        // 2、测试根据userId分表查询
        List<TOrder> list = orderService.lambdaQuery().eq(TOrder::getUserId, 7).list();
    }
    //----------------------------- 分表测试 end -----------------------------
3.3.4、测试分表效果

       先将application.yml里的active配置成fenbiao-qumo,这里可以观察到新增和查询都能根据user_id自动取模匹配对应表
在这里插入图片描述

3.4、分表 & 配置读写分离

       这里只有配置文件不同,其它逻辑和分表一致,只用将application.yml里的active配置成duxiefenli-fenbiao-qumo

3.4.1、准备分表 & 配置读写分离配置文件 application-duxiefenli-fenbiao-qumo.yml
# 读写分离+取模分表
spring:
  main:
    allow-bean-definition-overriding: true
  datasource:
    druid:
      access-to-underlying-connection-allowed: true
  shardingsphere:
    datasource:
      # 全部数据源名称 多个用逗号隔开
      names:
        master1,slave1,slave2
      # 主数据源
      master1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://172.16.8.181:3306/sharding-jdbc-test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
      # 从数据源
      slave1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://172.16.8.191:3306/sharding-jdbc-test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
      slave2:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://172.16.8.192:3306/sharding-jdbc-test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
    props:
      # 开启SQL显示,默认false
      sql:
        show: true

    sharding:
      # 读写分离配置
      master-slave-rules:
        ds0:
          master-data-source-name: master1
          slave-data-source-names: slave1, slave2
          # 负载均衡算法  ROUND_ROBIN:轮询  RANDOM:随机
          load-balance-algorithm-type: ROUND_ROBIN
      # 分表配置
      tables:
        t_order:
          # 由数据源名.表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式
          # ds0代表数据源名称,t_order表名称 ,$->{0..1} inline表达式代表取值0 1
          actual-data-nodes: ds0.t_order_$->{0..1}
          table-strategy:
            # 分表策略为inline
            inline:
              # 分表列名
              sharding-column: user_id
              # 分表算法 根据t_order表中的user_id取模2进行数据分片存储
              algorithm-expression: t_order_$->{user_id % 2}

3.5、分库分表

3.5.1、准备两个库在每个库中怎么两张表

       我这里安装了两MySQL,在两个MySQL中都创建一个叫sharding-jdbc-test的库,然后在库中创建两个分片表,t_order_0 t_order_1,也可以在一个MySQL中创建两个数据库测试效果是一样的。

CREATE TABLE `t_order_0` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_id` bigint DEFAULT NULL COMMENT '订单id(通过雪花算法生成)',
  `order_no` varchar(100) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `goods_info` varchar(100) DEFAULT NULL COMMENT '商品信息',
  `to_address` varchar(100) DEFAULT NULL COMMENT '收件地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
);
CREATE TABLE `t_order_1` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_id` bigint DEFAULT NULL COMMENT '订单id(通过雪花算法生成)',
  `order_no` varchar(100) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `goods_info` varchar(100) DEFAULT NULL COMMENT '商品信息',
  `to_address` varchar(100) DEFAULT NULL COMMENT '收件地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
);
3.5.2、准备分库分表配置文件 application-fenkufenbiao-qumo.yml
spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource:
      # 全部数据源名称 多个用逗号隔开
      names:
        master0,master1
      # 主数据源 数据源名称不能有下划线可以用 -
      master0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://172.16.8.181:3306/sharding-jdbc-test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
      master1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://172.16.8.182:3306/sharding-jdbc-test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
    props:
      # 开启SQL显示,默认false
      sql:
        show: true

    sharding:
      # 分库配置
      default-database-strategy:
        inline:
          # 分库依据字段
          sharding-column: user_id
          # 分库规则
          algorithm-expression: master$->{user_id % 2}
      # 分表配置
      tables:
        t_order:
          # 由数据源名.表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式
          # master代表数据源名称, $->{0..1} inline表达式代表取值0 1,user_info表名称 ,$->{0..1} inline表达式代表取值0 1
          actual-data-nodes: master$->{0..1}.t_order_$->{0..1}
          table-strategy:
            # 分表策略为inline
            inline:
              # 分表列名
              sharding-column: order_id
              # 分表算法 根据t_order表中的order_id取模2进行数据分片存储
              algorithm-expression: t_order_$->{order_id % 2}
          # 自定义t_order中order_id生成,使用雪花算法
          key-generator:
            column: order_id
            type: SNOWFLAKE
            props:
              worker:
                # 雪花算法的workId  机器为标识 0-1024
                id: 996

3.5.3、测试代码
    //----------------------------- 分库分表测试 start -----------------------------
    @Test
    public void fenkuFenbiaoTest(){
        // 1、测试自动主库写操作(在执行写操作时会自动选择主库)
        for (int i = 0; i < 10; i++) {
            TOrder tOrder = new TOrder();
            tOrder.setOrderNo("NO"+RandomUtil.randomNumbers(10));
            tOrder.setUserId(RandomUtil.randomInt(10));
            tOrder.setGoodsInfo("商品"+RandomUtil.randomString(5));
            tOrder.setToAddress("地址"+RandomUtil.randomString(5));
            tOrder.setCreateTime(new Date());
            orderService.save(tOrder);
        }
        // 2、测试根据userId分表查询
//        List<TOrder> list = orderService.lambdaQuery().eq(TOrder::getUserId, 7).list();
    }
    //----------------------------- 分表测试 end -----------------------------

3.5.4、测试分库分表效果

       先将application.yml里的active配置成fenkufenbiao-qumo,执行后可以看到根据我们配置的规则进行了分库分表。

在这里插入图片描述

3.6、自定义单字段分表策略(通过时间分表)

       Sharding-JDBC提供了表达式配置分表,同时也提供了自定义分表策略方式,不同业务的分表方案不同,常见的有取模、根据时间分表,这里会进行自定义时间分表策略演示,每个月分一张表。

3.6.1、准备分片表

       这里准备两张表t_order_2024_7 t_order_2024_8,用来演示自定义分表策略。

CREATE TABLE `t_order_2024_7` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_id` bigint DEFAULT NULL COMMENT '订单id(通过雪花算法生成)',
  `order_no` varchar(100) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `goods_info` varchar(100) DEFAULT NULL COMMENT '商品信息',
  `to_address` varchar(100) DEFAULT NULL COMMENT '收件地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
);
CREATE TABLE `t_order_2024_8` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_id` bigint DEFAULT NULL COMMENT '订单id(通过雪花算法生成)',
  `order_no` varchar(100) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `goods_info` varchar(100) DEFAULT NULL COMMENT '商品信息',
  `to_address` varchar(100) DEFAULT NULL COMMENT '收件地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
);
3.6.2、自定义单字段分表策略代码实现

       常用的自定义分表策略有两个,单字段分片和多字段分片,单字段分片会使用到,精确分片PreciseShardingAlgorithm,范围区间分片RangeShardingAlgorithm,这里会对订单创建时间进行两种分片策略实现。

3.6.2.1、精确分片策略实现
@Slf4j
public class OrderCreateTimePreciseShardingAlgorithm implements PreciseShardingAlgorithm<Date> {
    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {
        System.out.println("table PreciseShardingAlgorithm ");
        // 真实节点
        availableTargetNames.stream().forEach((item) -> {
            log.info("actual node table:{}", item);
        });

        log.info("logic table name:{},rout column:{}", shardingValue.getLogicTableName(), shardingValue.getColumnName());
        //精确分片
        log.info("column value:{}", shardingValue.getValue());
        
        String tb_name = shardingValue.getLogicTableName() + "_";

        // 根据当前日期 来 分库分表
        Date date = shardingValue.getValue();
        String year = String.format("%tY", date);
        String mon =String.valueOf(Integer.parseInt(String.format("%tm", date))); // 去掉前缀0

        // 选择表
        tb_name = tb_name + year + "_" + mon;
        System.out.println("tb_name:" + tb_name);

        for (String each : availableTargetNames) {
            System.out.println("t_order_:" + each);
            if (each.equals(tb_name)) {
                return each;
            }
        }
        throw new IllegalArgumentException();
    }
}
3.6.2.2、范围区间分片策略实现
@Slf4j
public class OrderCreateTimeRangeShardingAlgorithm implements RangeShardingAlgorithm<Date> {
    @Override
    public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Date> rangeShardingValue) {

        ArrayList<String> result = new ArrayList<>();

        //获取表名称
        String tb_name = rangeShardingValue.getLogicTableName() + "_";

        //获取查询时间
        Range<Date> valueRange = rangeShardingValue.getValueRange();
        Integer queryYearMonthBegin = null; // 查询开始年月
        if(valueRange.hasLowerBound()){
            Date beginDate = valueRange.lowerEndpoint();
            queryYearMonthBegin = Integer.valueOf(String.format("%tY", beginDate)+String.format("%tm", beginDate));
        }
        Integer queryYearMonthEnd = null; // 查询结束年月
        if(valueRange.hasUpperBound()){
            Date endDate = valueRange.upperEndpoint();
            queryYearMonthEnd = Integer.valueOf(String.format("%tY", endDate)+String.format("%tm", endDate));
        }


        //筛选需要查询的表
        for (String each : collection) {
            // 将表中的年月取出来用于判断
            String yearMonth = each.replace(tb_name, "");
            String[] yearMonthSplit = yearMonth.split("_");
            String year = yearMonthSplit[0];
            String mon = yearMonthSplit[1];
            mon = mon.length()==1?"0"+mon:mon;
            Integer yearMonthInt = Integer.parseInt(year+mon); // 数据表对应年月
            // 当范围查询时为封闭区间 且表的年月在查询区间内
            if(valueRange.hasLowerBound() && valueRange.hasUpperBound()){
                if(yearMonthInt>=queryYearMonthBegin && yearMonthInt<=queryYearMonthEnd){
                    result.add(each);
                }
            }
            // 当范围查询时为封闭区间
            if(valueRange.hasLowerBound() && !valueRange.hasUpperBound()){
                if(yearMonthInt>=queryYearMonthBegin){
                    result.add(each);
                }
            }
            // 当范围查询时为封闭区间
            if(!valueRange.hasLowerBound() && valueRange.hasUpperBound()){
                if(yearMonthInt<=queryYearMonthEnd){
                    result.add(each);
                }
            }
        }

        if(result.size()==0){
            log.error("查询表不存在");
            throw new IllegalArgumentException();
        }
        return result;
    }
}
3.6.3、准备自定义分表配置文件 application-zidingyi-fenbiao-date.yml
# 自定义时间分表配置
# 自定义时间分表配置
spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource:
      # 全部数据源名称 多个用逗号隔开
      names:
        master0
      # 主数据源
      master0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://172.16.8.181:3306/sharding-jdbc-test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
    props:
      # 开启SQL显示,默认false
      sql:
        show: true

    # 分表配置
    sharding:
      tables:
        t_order:
          # 由数据源名.表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式
          # master代表数据源名称,t_order表名称 ,$->{7..8} inline表达式代表取值7 8
          actual-data-nodes: master0.t_order_$->{2024..2024}_$->{7..8}
          table-strategy:
            # 分表策略为 自定义策略
            standard:
              # 分表列名
              sharding-column: create_time
              # 自定义精确分表算法类路径
              precise-algorithm-class-name: com.kerwin.config.OrderCreateTimePreciseShardingAlgorithm
              # 自定义范围分片算法类名称,用于BETWEEN,可选。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器
              range-algorithm-class-name: com.kerwin.config.OrderCreateTimeRangeShardingAlgorithm
          # 自定义t_order中order_id生成,使用雪花算法
          key-generator:
            column: order_id
            type: SNOWFLAKE
            props:
              worker:
                # 雪花算法的workId  机器为标识 0-1024
                id: 996
3.6.4、测试代码
    //----------------------------- 自定义分表策略测试 start -----------------------------
    @Test
    public void zidinyiFenbiaoTest(){
        // 1、插入数据
        for (int i = 0; i < 10; i++) {
            TOrder tOrder = new TOrder();
            tOrder.setOrderNo("NO"+RandomUtil.randomNumbers(10));
            tOrder.setUserId(RandomUtil.randomInt(10));
            tOrder.setGoodsInfo("商品"+RandomUtil.randomString(5));
            tOrder.setToAddress("地址"+RandomUtil.randomString(5));
            tOrder.setCreateTime(new Date());
            orderService.save(tOrder);
        }
//        // 2、根据时间精确查询
//        List<TOrder> list = orderService.lambdaQuery()
//                .eq(TOrder::getCreateTime, DateUtil.parseDate("2024-08-07 00:00:00"))
//                .list();
//
//        // 3、根据时间范围查询
//        List<TOrder> list1 = orderService.lambdaQuery()
//                .ge(TOrder::getCreateTime, DateUtil.parseDate("2024-07-01 00:00:00"))
//                .le(TOrder::getCreateTime, DateUtil.parseDate("2024-08-30 23:59:59"))
//                .list();
    }
    //----------------------------- 自定义分表策略测试 end -----------------------------
3.6.4、测试自定义分表策略效果
3.6.4.1、插入数据查看分片表选择

       执行插入数据可以观察到选择到了t_order_2024_8表。

在这里插入图片描述

3.6.4.2、根据时间精确查询查看分片表选择

       根据时间精确查询可以观察到选择到了t_order_2024_8表。

在这里插入图片描述

3.6.4.3、根据时间范围查询查看分片表选择

       根据时间范围查询可以观察到选择到了t_order_2024_7 t_order_2024_8表。

在这里插入图片描述

3.7、自定义复杂分片策略(多字段分片,这里会使用user_id + create_time进行分片)

       Sharding-JDBC提供了表达式配置分表,同时也提供了自定义分表策略方式,不同业务的分表方案不同,常见的有取模、根据时间分表,这里会进行自定义时间分表策略演示,每个月分一张表。

3.7.1、准备分片表

       这里准备两张表t_order_0_2024 t_order_1_2025,用来演示自定义分表策略。

CREATE TABLE `t_order_0_2024` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_id` bigint DEFAULT NULL COMMENT '订单id(通过雪花算法生成)',
  `order_no` varchar(100) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `goods_info` varchar(100) DEFAULT NULL COMMENT '商品信息',
  `to_address` varchar(100) DEFAULT NULL COMMENT '收件地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
);
CREATE TABLE `t_order_1_2024` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_id` bigint DEFAULT NULL COMMENT '订单id(通过雪花算法生成)',
  `order_no` varchar(100) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `goods_info` varchar(100) DEFAULT NULL COMMENT '商品信息',
  `to_address` varchar(100) DEFAULT NULL COMMENT '收件地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
);
CREATE TABLE `t_order_0_2025` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_id` bigint DEFAULT NULL COMMENT '订单id(通过雪花算法生成)',
  `order_no` varchar(100) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `goods_info` varchar(100) DEFAULT NULL COMMENT '商品信息',
  `to_address` varchar(100) DEFAULT NULL COMMENT '收件地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
);
CREATE TABLE `t_order_1_2025` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_id` bigint DEFAULT NULL COMMENT '订单id(通过雪花算法生成)',
  `order_no` varchar(100) NOT NULL COMMENT '订单编号',
  `user_id` bigint NOT NULL COMMENT '用户id',
  `goods_info` varchar(100) DEFAULT NULL COMMENT '商品信息',
  `to_address` varchar(100) DEFAULT NULL COMMENT '收件地址',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
);
3.7.2、自定义多字段分表策略代码实现

       复杂分表会使用到ComplexKeysShardingAlgorithm,精确查询和范围查询都在这一个接口中实现,这里只做简单实现。

@Slf4j
public class OrderUserIdYearComplexShardingStrategy implements ComplexKeysShardingAlgorithm {

    @Override
    public Collection<String> doSharding(Collection collection, ComplexKeysShardingValue complexKeysShardingValue) {

        LinkedHashSet<String> resultTableNameSet = new LinkedHashSet<>();

        //获取表名称
        String tb_name = complexKeysShardingValue.getLogicTableName() + "_";

        // 精确分片字段
        Map<String, List> columnNameAndShardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
        List<Integer> userIdList = columnNameAndShardingValuesMap.get("user_id");
        List<Date> createTimeList = columnNameAndShardingValuesMap.get("create_time");

        // 如果user_id和create_time都是精确分片特殊处理,插入的时候这两个字段都是精确分片
        if(CollectionUtil.isNotEmpty(userIdList) && CollectionUtil.isNotEmpty(createTimeList)){
            Date date = createTimeList.get(0);
            String year = String.format("%tY", date);
            resultTableNameSet.add(tb_name + userIdList.get(0)%2 + "_" + year);
            return resultTableNameSet;
        }

        // 范围分片字段
        Map<String, Range> columnNameAndRangeValuesMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();
        Range<Date> createTimeRange = columnNameAndRangeValuesMap.get("create_time");
        Integer queryYearBegin = null; // 查询开始年
        if (createTimeRange!=null && createTimeRange.hasLowerBound()) {
            Date beginDate = createTimeRange.lowerEndpoint();
            queryYearBegin = Integer.valueOf(String.format("%tY", beginDate));
        }
        Integer queryYearEnd = null; // 查询结束年
        if (createTimeRange!=null && createTimeRange.hasUpperBound()) {
            Date endDate = createTimeRange.upperEndpoint();
            queryYearEnd = Integer.valueOf(String.format("%tY", endDate));
        }

        // 先对user_id进行取模分片 得到一个结果数组
        List<String> oneTableList = new ArrayList<>();
        for (Integer userId : userIdList) {
            oneTableList.add(tb_name + userId % 2 + "_");
        }

        // 再取出范围查询create_time的年分组装最终表名称集合
        for (String each : (Collection<String>) collection) {
            // 获取存在表的年份
            Integer eachYear = null;
            for (String oneTable : oneTableList) {
                if(each.contains(oneTable)){
                    eachYear = Integer.valueOf(each.replace(oneTable, ""));
                    break;
                }
            }
            if(eachYear == null){
                continue;
            }
            // 当没有时间范围查询时
            if(createTimeRange == null){
                resultTableNameSet.add(each);
                continue;
            }

            // 当范围查询时为封闭区间 且表的年月在查询区间内
            if(createTimeRange.hasLowerBound() && createTimeRange.hasUpperBound()){
                if(eachYear>=queryYearBegin && eachYear<=queryYearEnd){
                    resultTableNameSet.add(each);
                    continue;
                }
            }
            // 当范围查询时为封闭区间
            if(createTimeRange.hasLowerBound() && !createTimeRange.hasUpperBound()){
                if(eachYear>=queryYearBegin){
                    resultTableNameSet.add(each);
                    continue;
                }
            }
            // 当范围查询时为封闭区间
            if(!createTimeRange.hasLowerBound() && createTimeRange.hasUpperBound()){
                if(eachYear<=queryYearEnd){
                    resultTableNameSet.add(each);
                    continue;
                }
            }
        }

        if (resultTableNameSet.size() == 0) {
            log.error("查询表不存在");
            throw new IllegalArgumentException();
        }

        return resultTableNameSet;
    }
}
3.7.3、准备自定义复杂分表配置文件 application-zidingyi-fenbiao-complex.yml
# 自定义时间分表配置
spring:
  main:
    allow-bean-definition-overriding: true
  shardingsphere:
    datasource:
      # 全部数据源名称 多个用逗号隔开
      names:
        master0
      # 主数据源
      master0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://172.16.8.181:3306/sharding-jdbc-test?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: 123456
    props:
      # 开启SQL显示,默认false
      sql:
        show: true

    # 分表配置
    sharding:
      tables:
        t_order:
          # 由数据源名.表名组成,以小数点分隔。多个表以逗号分隔,支持inline表达式
          # master代表数据源名称,t_order表名称 ,$->{0..1} inline表达式代表取值0 1
          actual-data-nodes: master0.t_order_$->{0..1}_$->{2024..2025}
          table-strategy:
            # 分表策略为 自定义复杂分片策略
            complex:
              # 分表列名 多个用,隔开
              sharding-columns: user_id,create_time
              # 自定义复杂分表算法类路径 这个里面包含精确分片和范围分片
              algorithm-class-name: com.kerwin.config.OrderUserIdYearComplexShardingStrategy
3.7.4、测试代码
    //----------------------------- 多键分表测试 start -----------------------------
    @Test
    public void fenbiaoDuojianTest(){
        // 1、测试自动主库写操作(在执行写操作时会自动选择主库)
        for (int i = 0; i < 10; i++) {
            TOrder tOrder = new TOrder();
            tOrder.setOrderNo("NO"+RandomUtil.randomNumbers(10));
            tOrder.setUserId(RandomUtil.randomInt(10));
            tOrder.setGoodsInfo("商品"+RandomUtil.randomString(5));
            tOrder.setToAddress("地址"+RandomUtil.randomString(5));
            tOrder.setCreateTime(new Date());
            orderService.save(tOrder);
        }

        // 2、测试根据userId+时间分表查询
//        List<TOrder> list = orderService.lambdaQuery()
//                .eq(TOrder::getUserId, 7)
//                .eq(TOrder::getCreateTime,DateUtil.parseDate("2025-08-09 23:59:59"))
//                .list();

        // 3、测试根据userId+时间范围分表查询
//        List<TOrder> list = orderService.lambdaQuery()
//                .eq(TOrder::getUserId, 7)
//                .between(TOrder::getCreateTime,DateUtil.parseDate("2024-08-09 00:00:00"),DateUtil.parseDate("2025-08-09 23:59:59"))
//                .list();

    }
    //----------------------------- 多键分表测试 end -----------------------------

3.7.5、测试自定义分表策略效果

       先将application.yml里的active配置成zidingyi-fenbiao-complex,执行后可以看到根据我们配置的规则进行了分库分表。

3.7.5.1、插入数据查看分片表选择

       执行插入数据可以观察到选择到了正常分片到了t_order_0_2024 t_order_1_2024表。

在这里插入图片描述

3.7.5.2、根据用户ID+时间精确查询查看分片表选择

       根据用户ID+时间精确查询可以观察到选择到了t_order_1_2025表。

在这里插入图片描述

3.7.5.3、根据时间范围查询查看分片表选择

       根据时间范围查询可以观察到选择到了t_order_1_2024 t_order_1_2025表。

在这里插入图片描述

四、总结

       这里只演示了部分情况,还有像多表关联查询、分页查询等都是可以适配的,在分页查询中有个问题,如果在分库或分表的情况命中多个分片因为数据在不同表中获取结果集是有问题的,因为普通的分页查询没法确定多张表的顺序问题,比如一个表有两个分片,查询第3页每页3条数据,如果没有做分表那么在数据库一共会扫描9条数据取出第7-9条,而在分表的情况下则不能只取出每个分片中的第7-9条数据,必须将1-9条数据都取出来进行应用层归并处理,Sharding-JDBC已经将这个功能实现并且进行了优化,虽然做了优化但是不可避免分页查询还是存在的性能问题,要想解决这个问题也有很多方法这里不做展开。

在这里插入图片描述

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值