Sharding-JDBC分库分表-自定义分片算法-4

默认分片算法

Sharding JDBC通过org.apache.shardingsphere.sharding.spi.ShardingAlgorithm接口定义了数据分片算法,5.2.1版本默认提供了如下的分片算法

配置标识 自动分片算法 详细说明 类名
MOD Y 基于取模的分片算法 ModShardingAlgorithm
HASH_MOD Y 基于哈希取模的分片算法 HashModShardingAlgorithm
BOUNDARY_RANGE Y 基于分片边界的范围分片算法 BoundaryBasedRangeShardingAlgorithm
VOLUME_RANGE Y 基于分片容量的范围分片算法 VolumeBasedRangeShardingAlgorithm
AUTO_INTERVAL Y 基于可变时间范围的分片算法 AutoIntervalShardingAlgorithm
INTERVAL N 基于固定时间范围的分片算法 IntervalShardingAlgorithm
CLASS_BASED N 基于自定义类的分片算法 ClassBasedShardingAlgorithm
INLINE N 基于行表达式的分片算法 InlineShardingAlgorithm
COMPLEX_INLINE N 基于行表达式的复合分片算法 ComplexInlineShardingAlgorithm
HINT_INLINE N 基于行表达式的 Hint 分片算法 HintInlineShardingAlgorithm

默认算法的继承关系如下

Sharding-JDBC分库分表-自定义分片算法-4_第1张图片

分片算法参考官方用户文档:默认分片算法介绍

自定义分片算法

自定义分片算法时通过配置分片策略类型和算法类名,实现自定义扩展。 CLASS_BASED 允许向算法类内传入额外的自定义属性,传入的属性可以通过属性名为 propsjava.util.Properties 类实例取出。

自定义分片算法有三种类型

  • 标准分片算法
  • 复杂分片算法
  • hint分片算法

对应需要实现的接口分别为:

算法分类 需要实现接口 说明
标准 StandardShardingAlgorithm 支持单个分片键,需要实现精确和范围分片接口
复杂 ComplexShardingAlgorithm 支持多个分片键,但是分片键数据类型需要一样
hint HintShardingAlgorithm 没有分片键,分片值通过hint注入而不是SQL

分片算法开发

以标准算法为例,对下面的order_t进行分表

CREATE TABLE `order_t` (
  `order_id` bigint(20) NOT NULL COMMENT 'order_id主键',
  `order_no` varchar(32) DEFAULT NULL COMMENT '订单编号',
  `user_id` bigint(10) NOT NULL COMMENT '用户ID',
  `order_date` date NOT NULL COMMENT '下单时间',
  `order_amount` decimal(16,2) NOT NULL COMMENT '订单金额',
  `delivery_amount` decimal(16,2) DEFAULT '0.00' COMMENT '运费',
  `total_amount` decimal(16,2) NOT NULL COMMENT '汇总金额',
  `receiver_id` bigint(10) NOT NULL COMMENT '收货地址ID',
  `status` tinyint(4) DEFAULT '1' COMMENT '状态,1:已提交,2:已付款,3:待发货,4:已发货,5:已收货,6:已完成',
  `deleted` tinyint(4) DEFAULT '0' COMMENT '删除标志,0:未删除,1:已删除',
  `create_by` bigint(10) DEFAULT NULL COMMENT '创建人',
  `creation_date` datetime DEFAULT NULL COMMENT '创建时间',
  `last_update_by` bigint(10) DEFAULT NULL COMMENT '修改人',
  `last_update_date` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`order_id`),
  KEY `idx_useId` (`user_id`),
  KEY `idx_orderNo` (`order_no`),
  KEY `idx_orderDate` (`order_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表';

分表规则为

  • 按照下单时间order_date分表
  • 一个季度分一个表
  • 只分2023年

目标表有:order_t1,order_t2,order_t3,order_t4

配置如下:

spring:
  shardingsphere:
    rules:
      sharding:
        tables: # 需要分库表的规则配置
          order_t:
            actual-data-nodes: ds0.order_t$->{1..4}  # 待选数据节点:ds0.order_t1、ds0.order_t2、ds0.order_t3
            key-generate-strategy:  # 分布式ID列,一般是主键
              column: order_id
              key-generator-name: beautySnowflake # 使用自定义分布式ID算法是
            table-strategy: # 分库策略配置
              standard: # 标准算法
                sharding-column: order_date # 分片列
                sharding-algorithm-name: quarter_std # 分片算法
        key-generators: # 分布式ID生成算法
          beautySnowflake:
            type: BEAUTY_SNOWFLAKE
        sharding-algorithms: # 分片算法,配置后可以在分片表的分片策略中被引用
          quarter_std:
            type: CLASS_BASED
            props:
              strategy: standard # 标准算法
              algorithmClassName: com.xlt.sharding.startegy.DateStdShardingAlgorithm # 自定义算法类路径
              lowerLimit: "2023-01-01 00:00:00" # 分片时间下限
              upperLimit: "2023-12-31 24:00:00" # 分片时间上限
              interval: 3  # 分片间隔月数

自定义标准分片算法DateStdShardingAlgorithm需要实现StandardShardingAlgorithm接口,实现其精确分片和范围分片doSharding方法,除此之外还要实现init、getProps、getType等方法。

@Slf4j
public class DateStdShardingAlgorithm implements StandardShardingAlgorithm<Date> {

    private Date lowerDate;

    private Date upperDate;

    private Integer interval;

    private Properties properties;

    private static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";

    @Override
    public void init(Properties properties) {
        String lowerLimit = (String) properties.get("lowerLimit");
        String upperLimit = (String) properties.get("upperLimit");
        String interval = (String) properties.get("interval");
        AssertUtil.isNull(lowerLimit, "lowerLimit can't be empty");
        AssertUtil.isNull(upperLimit, "upperLimit can't be empty");
        AssertUtil.isNull(interval, "interval can't be empty");
        this.lowerDate = parseDate(lowerLimit);
        this.upperDate = parseDate(upperLimit);
        this.interval = Integer.parseInt(interval);
        this.properties = properties;
    }

    private Date parseDate(String lowerLimit) {
        Date date = null;
        SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN);
        try {
            date = sdf.parse(lowerLimit);
        } catch (ParseException e) {
            log.error("String date parse error:", e);
            throw new CommonException(e.getMessage());
        }
        return date;
    }

    private String formatDate(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN);
        return sdf.format(date);
    }

    private int getYear(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        return calendar.get(Calendar.YEAR);
    }

    private int getMonth(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        return calendar.get(Calendar.MONTH) + 1;
    }

    /**
     * Get properties.
     *
     * @return properties
     */
    @Override
    public Properties getProps() {
        return properties;
    }

    /**
     * Get type.
     *
     * @return type
     */
    @Override
    public String getType() {
        return "QUARTER_DATE";
    }

    /**
     * Sharding.
     *
     * @param tableNames    available data sources or table names
     * @param shardingValue sharding value
     * @return sharding result for data source or table name
     */
    @Override
    public String doSharding(Collection<String> tableNames, PreciseShardingValue<Date> shardingValue) {
        log.info("tableNames={},shardingValue={},properties={}", JSON.toJSONString(tableNames), JSON.toJSONString(shardingValue), JSON.toJSONString(properties));
        Date date = shardingValue.getValue();
        AssertUtil.isTrue(date.getTime() < lowerDate.getTime(), formatDate(date) + " is before lowerLimit: " + formatDate(lowerDate));
        AssertUtil.isTrue(date.getTime() > upperDate.getTime(), formatDate(date) + " is after upperLimit: " + formatDate(upperDate));
        int idx = calTableIdx(date);
        log.info("idx={}", idx);
        String targetTable = "";
        for (String tableName : tableNames) {
            String tblIdxStr = tableName.substring(tableName.indexOf("t") + 1);
            int tblIdx = Integer.parseInt(tblIdxStr);
            if (tblIdx == idx) {
                targetTable = tableName;
                break;
            }
        }
        log.info("targetTable={}", targetTable);
        return targetTable;
    }

    private int calTableIdx(Date date) {
        int months = (getYear(date) - getYear(lowerDate)) * 12 + getMonth(date);
        int flag = months % interval == 0 ? 0 : 1;
        return months / interval + flag;
    }

    /**
     * Sharding.
     *
     * @param tableNames    available data sources or table names
     * @param shardingValue sharding value
     * @return sharding results for data sources or table names
     */
    @Override
    public Collection<String> doSharding(Collection<String> tableNames, RangeShardingValue<Date> shardingValue) {
        log.info("tableNames={},shardingValue={},properties={}", JSON.toJSONString(tableNames), JSON.toJSONString(shardingValue), JSON.toJSONString(properties));
        Date lowDate = shardingValue.getValueRange().lowerEndpoint();
        Date upDate = shardingValue.getValueRange().upperEndpoint();
        AssertUtil.isTrue(lowDate.getTime() > upDate.getTime(), formatDate(lowDate) + " is after upperEndpoint: " + formatDate(upDate));
        AssertUtil.isTrue(lowDate.getTime() < lowerDate.getTime(), formatDate(lowDate) + " is before lowerLimit: " + formatDate(lowerDate));
        AssertUtil.isTrue(upDate.getTime() > upperDate.getTime(), formatDate(upDate) + " is after upperLimit: " + formatDate(upperDate));
        int lowIdx = calTableIdx(lowDate);
        int upIdx =  calTableIdx(upDate);
        log.info("lowIdx={},upIdx={}", lowIdx, upIdx);
        List<String> targetTbls = new ArrayList<>();
        for (String tableName : tableNames) {
            String dsIdxStr = tableName.substring(tableName.indexOf("t") + 1);
            int dsIdx = Integer.parseInt(dsIdxStr);
            if (dsIdx >= lowIdx && dsIdx <= upIdx) {
                targetTbls.add(tableName);
            }
        }
        log.info("target table names={}", targetTbls);
        return targetTbls;
    }
}

测试用例设计

针对以上的分片算法设计测试用例对其进行验证

1、新增订单数据

使用JMockData和Faker生成100条随机数据进行插入测试

@SpringBootTest
@Slf4j
public class ShardingJdbcTest {

    @Autowired
    private IOrderMapper mapper;

    /**
     * 新增订单
     */
    @Test
    public void addOrder() {
        for (int i = 0; i < 100; i++) {
            OrderPo orderPo = JMockData.mock(OrderPo.class);
            orderPo.setOrderId((long) i);
            orderPo.setStatus(1);
            orderPo.setDeleted(0);
            Faker faker = new Faker();
            Calendar fromDate = Calendar.getInstance();
            fromDate.set(2023, Calendar.JANUARY, 1);
            Calendar toDate = Calendar.getInstance();
            toDate.set(2023, Calendar.DECEMBER, 31);
            orderPo.setOrderDate(faker.date().between(fromDate.getTime(), toDate.getTime()));
            log.info("add new order:{}", orderPo);
            mapper.insert(orderPo);
        }
    }
}

生成数据情况,order_t1中order_date为1-3月:

Sharding-JDBC分库分表-自定义分片算法-4_第2张图片

order_t2中order_date为4-6月:

2、查询订单数据-等值查询

使用分片键查询单一时间的数据

    /**
     * 单个查询
     */
    @Test
    public void queryOrder_1() {
        QueryWrapper<OrderPo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("order_date", DateUtil.parseDate("2023-03-12", DateUtil.DATE_PATTERN_2));
        List<OrderPo> orderPos = mapper.selectList(queryWrapper);
        log.info("orderPos={}", JSON.toJSONString(orderPos));
    }

从日志中看,找到目标表之后,查到了对应的数据

Sharding-JDBC分库分表-自定义分片算法-4_第3张图片

3、范围查询订单数据

使用分片键查询一定时间范围内的数据

/**
 * 范围查询
 */
@Test
public void queryOrder_2() {
    QueryWrapper<OrderPo> queryWrapper = new QueryWrapper<>();
    queryWrapper.between("order_date", DateUtil.parseDate("2023-05-01", DateUtil.DATE_PATTERN_2), DateUtil.parseDate("2023-08-01", DateUtil.DATE_PATTERN_2));
    List<OrderPo> orderPos = mapper.selectList(queryWrapper);
    log.info("orderPos={}", JSON.toJSONString(orderPos));
}

可以看到查询过程中,根据order_date的范围,经过分片算法的计算,路由到了order_t_2和order_t_3表,然后查回相应的数据

Sharding-JDBC分库分表-自定义分片算法-4_第4张图片

你可能感兴趣的:(分库分表,分开分表,自定义分片算法,sharding,JDBC)