文章目录
  1. 1. 了解Sharding-JDBC的数据分片策略
    1. 1.1. 精确分片算法–PreciseShardingAlgorithm
    2. 1.2. 范围分片算法–RangeShardingAlgorithm
    3. 1.3. 复合分片算法–ComplexKeysShardingAlgorithm
    4. 1.4. Hint分片算法–HintShardingAlgorithm
    5. 1.5. 标准分片策略–StandardShardingStrategy
    6. 1.6. 复合分片策略–ComplexShardingStrategy
    7. 1.7. 行表达式分片策略–InlineShardingStrategy
    8. 1.8. Hint分片策略–HintShardingStrategy
    9. 1.9. 不分片策略–NoneShardingStrategy
  2. 2. 实战–自定义复合分片策略
    1. 2.1. 场景回顾
    2. 2.2. 开发自定义复合分库策略
      1. 2.2.1. 核心逻辑
      2. 2.2.2. 核心逻辑解析
    3. 2.3. 开发自定义复合分表策略
      1. 2.3.1. 核心逻辑解析
    4. 2.4. 测试前置–配置自定义分库分表策略
    5. 2.5. 测试用例A–新增订单数据
    6. 2.6. 测试用例B–通过订单号查询订单明细
  3. 3. 小结
  4. 4. 附录:解析枚举类的values()方法

本文是 “跟我学Sharding-JDBC” 系列的第四篇,我将带领读者一起了解下Sharding-JDBC的数据分片规则并通过实例实现自定义分片策略的开发实现。

Sharding-JDBC中的分片策略有两个维度,分别是:数据源分片策略(DatabaseShardingStrategy)、表分片策略(TableShardingStrategy)。

其中,数据源分片策略表示:数据路由到的物理目标数据源,表分片策略表示数据被路由到的目标表。

特别的,表分片策略是依赖于数据源分片策略的,也就是说要先分库再分表。

这里贴一张盗来的图

Sharding-JDBC分片策略代码架构

了解Sharding-JDBC的数据分片策略

Sharding-JDBC的分片策略包含了分片键和分片算法。由于分片算法与业务实现紧密相关,因此Sharding-JDBC没有提供内置的分片算法,而是通过分片策略将各种场景提炼出来,提供了高层级的抽象,通过提供接口让开发者自行实现分片算法。

以下内容引用自官方文档。官方文档

首先介绍四种分片算法。

通过分片算法将数据分片,支持通过=、BETWEEN和IN分片。
分片算法需要应用方开发者自行实现,可实现的灵活度非常高。

目前提供4种分片算法。由于分片算法和业务实现紧密相关,
因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,
提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。

精确分片算法–PreciseShardingAlgorithm

用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用。

范围分片算法–RangeShardingAlgorithm

用于处理使用单一键作为分片键的BETWEEN AND进行分片的场景。需要配合StandardShardingStrategy使用。

复合分片算法–ComplexKeysShardingAlgorithm

用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合ComplexShardingStrategy使用。

: 我们在业务开发中,经常有根据用户id 查询某用户的记录列表,又有根据某个业务主键查询该用户的某记录的需求,这就需要用到复合分片算法。比如,订单表中,我们既需要查询某个userId的某时间段内的订单列表数据,又需要根据orderId查询某条订单数据。这里,orderId与userId就属于复合分片键。

Hint分片算法–HintShardingAlgorithm

Hint分片指的是对于分片字段非SQL决定,而由其他外置条件决定的场景,可以通过使用SQL Hint灵活注入分片字段。

Hint分片策略是绕过SQL解析的,因此能够通过实现该算法来实现Sharding-JDBC不支持的语法限制。

用于处理使用Hint行分片的场景。需要配合HintShardingStrategy使用。

接着介绍下五种分片策略。

标准分片策略–StandardShardingStrategy

提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。

复合分片策略–ComplexShardingStrategy

提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。

这里体现出框架设计者对设计原则的透彻理解,将变更点暴露给用户,将不变的封装在内部,明确的划分了抽象和实现的界限,这是值得我们学习的。

行表达式分片策略–InlineShardingStrategy

使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: tuser$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0到t_user_7。

上一篇文章中,我就是使用这个方式进行了demo的开发和讲解,对于快速体验Sharding-JDBC的魅力是很有意义的,但是这种方式对于复杂的业务支持程度就差一些,因此实际的业务开发中还是推荐使用复合分片策略–ComplexShardingStrategy。

Hint分片策略–HintShardingStrategy

通过Hint而非SQL解析的方式分片的策略。

不分片策略–NoneShardingStrategy

该策略为不分片的策略。

实战–自定义复合分片策略

由于目的为贴近实战,因此着重讲解如何实现复杂分片策略,即实现ComplexShardingStrategy接口定制生产可用的分片策略。

场景回顾

首先回顾一下业务场景,我们对订单进行分库分表,分为4库8表,复合分片键为user_id及order_id。

对用户进行分库分表,分为4库16表,分片键为user_id。

对订单表进行分库分表,分为4库8表,分片键为order_id,查询条件为user_id、order_id。

我们的业务流程如下:

  1. 根据一个外部路由id(如:支付宝uid、微信openId等)生成系统内部的用户user_id
  2. 根据系统内部user_id生成业务id,如:order_id、account_id等
  3. 根据外部id查询,获得系统内部user_id
  4. 根据系统内部user_id查询用户的所有订单信息
  5. 根据订单号order_id查询单条订单明细数据

在上篇文章 跟我学shardingjdbc之分布式主键及其自定义 中,我们完成了自定义分布式主键的生成,本节中,我将基于该分布式主键生成规则,配合Sharding-JDBC的复合分片策略接口,开发符合上述业务流程的复合分片规则,该规则已经在我们线上稳定运行,读者朋友可以借鉴并运用到自己的生产环境。

开发自定义复合分库策略

我们需要开发复合分库、分表两个策略,首先开发分库策略。

定义自定义复合分库策略实现类:SnoWalkerComplexShardingDB.java,实现 ComplexKeysShardingAlgorithm 接口。需要重写其 doSharding(Collection availableTargetNames, Collection shardingValues) 方法。

doSharding(Collection availableTargetNames, Collection shardingValues) 方法返回值为:物理数据源、物理表分片结果,如:ds0, t_user_0000,Sharding-JDBC会将数据路由至物理分片。

核心逻辑

这里是方法的关键逻辑

/**
 * @param availableTargetNames 可用数据源集合
 * @param shardingValues   分片键
 * @return sharding results for data sources or tables's names
 */
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, Collection<ShardingValue> shardingValues) {

    // 0. 打印数据源集合 及 分片键属性集合
    log.info("availableTargetNames:" + JSON.toJSONString(availableTargetNames) + ",shardingValues:" + JSON.toJSONString(shardingValues));
    // availableTargetNames:["ds0","ds1","ds2","ds3"],
    // shardingValues:[{"columnName":"user_id","logicTableName":"t_new_order","values":["UD020003011903261545436593200002"]},
    //                {"columnName":"order_id","logicTableName":"t_new_order","values":["OD000000011903261545475143200001"]}]
    List<String> shardingResults = new ArrayList<>();

    // 1. 遍历分片键集合,匹配数据源
    for (ShardingValue var : shardingValues) {

        ListShardingValue<String> listShardingValue = (ListShardingValue<String>)var;
        List<String> shardingValue = (List<String>)listShardingValue.getValues();

        // shardingValue:["UD020003011903261545436593200002"]
        log.info("shardingValue:" + JSON.toJSONString(shardingValue));

        // 2. 获取数据源索引值
        String index = getIndex(listShardingValue.getLogicTableName(),
                                listShardingValue.getColumnName(),
                                shardingValue.get(0));

        // 3. 循环匹配数据源,匹配到则退出循环
        for (String name : availableTargetNames) {
            // 4. 获取逻辑数据源索引后缀,即 0,1,2,3
            String nameSuffix = name.substring(ShardingConstant.LOGIC_DB_PREFIX_LENGTH);
            // 5. 当且仅当availableTargetNames中的数据源索引与路由值对应的分片索引相同退出循环
            if (nameSuffix.equals(index)) {
                // 6. 添加到分片结果集合
                shardingResults.add(name);
                break;
            }
        }

        //匹配到一种路由规则就可以退出
        if (shardingResults.size() > 0) {
            break;
        }
    }

    return shardingResults;
}

核心逻辑解析

梳理一下逻辑,首先介绍一下该方法的入参

参数名 解释
availableTargetNames 有效的物理数据源,即配置文件中的 ds0,ds1,ds2,ds3
shardingValues 分片属性,如:{“columnName”:”user_id”,”logicTableName”:”t_new_order”,”values”:[“UD020003011903261545436593200002”]} ,包含:分片列名,逻辑表名,当前列的具体分片值

该方法返回值为

参数名 解释
Collection<String> 分片结果,可以是目标数据源,也可以是目标数据表,此处为数据源

接着回来看业务逻辑,伪代码如下

  1. 首先打印了一下数据源集合 availableTargetNames 以及 分片属性 shardingValues的值,执行测试用例后,日志输出为:

    availableTargetNames:["ds0","ds1","ds2","ds3"],
    shardingValues:[{"columnName":"user_id","logicTableName":"t_new_order","values":["UD020003011903261545436593200002"]},
                    {"columnName":"order_id","logicTableName":"t_new_order","values":["OD000000011903261545475143200001"]}]
    

从日志可以看出,我们可以在该路由方法中取到配置时的物理数据源列表,以及在运行时获取本次执行时的路由属性及其值

完整的逻辑流程如下:

  1. 定义一个集合用于放置最终匹配好的路由数据源,接着对shardingValues进行遍历,目的为至少命中一个路由键
  2. 遍历shardingValues循环体中,打印了当前循环的shardingValue,即实际的分片键的数值,如:订单号、用户id等。通过getIndex方法,获取该分片键值中包含的物理数据源索引
  3. 接着遍历数据源列表availableTargetNames,截取当前循环对应availableTargetName的索引值,(eg: ds0则取0,ds1则取1…以此类推)将该配置的物理数据源索引与 第2步 中解析到的数据源路由索引进行比较,两者相等则表名我们期望将该数据路由到该匹配到的数据源。
  4. 执行这个过程,直到匹配到一个路由键则停止循环,之所以这么做是因为我们是复合分片,至少要匹配到一个路由规则,才能停止循环,最终将路由到的物理数据源(ds0/ds1/ds2/ds3)通过add方法添加到事先定义好的集合中并返回给框架。
  5. 逻辑结束。

可能读者朋友对如何从shardingValue中解析数据源的索引不理解,这里讲解一下。

上一篇文章中,我们在自定义全局业务id时,定义了一个主键枚举,并在枚举中定义了主键中库、表索引存放的位置,详细内容请移步 跟我学shardingjdbc之分布式主键及其自定义 , 到这里就简单了,Sharding-JDBC框架已经通过 doSharding(Collection availableTargetNames, Collection shardingValues) 方法将当前的路由键的值给了我们,也就是我们通过KeyGenerator生成的业务主键,我们只需要解析该主键,获取其中的库索引即可,代码逻辑如下:

/**
 * 根据分片键计算分片节点
 * @param logicTableName
 * @param columnName
 * @param shardingValue
 * @return
 */
public String getIndex(String logicTableName, String columnName, String shardingValue) {
    String index = "";
    if (StringUtils.isBlank(shardingValue)) {
        throw new IllegalArgumentException("分片键值为空");
    }
    //截取分片键值-下标循环主键规则枚举类,匹配主键列名得到规则
    for (DbAndTableEnum targetEnum : DbAndTableEnum.values()) {

        /**目标表路由
         * 如果逻辑表命中,判断路由键是否与列名相同
         */
        if (targetEnum.getTableName().equals(logicTableName)) {
            //目标表的目标主键路由-例如:根据订单id查询订单信息
            if (targetEnum.getShardingKey().equals(columnName)) {
                index = getDbIndexBySubString(targetEnum, shardingValue);
            }else{
                //目标表的非目标主键路由-例如:根据内部用户id查询订单信息-内部用户id路由-固定取按照用户表库表数量
                //兼容且仅限根据外部id路由 查询用户信息
                index = getDbIndexByMod(targetEnum, shardingValue);
            }
            break;
        }
    }
    if (StringUtils.isBlank(index)) {
        String msg = "从分片键值中解析数据库索引异常:logicTableName=" + logicTableName + "|columnName=" + columnName + "|shardingValue=" + shardingValue;
        throw new IllegalArgumentException(msg);
    }
    return index;
}

分析一下逻辑:

  1. 由于我们无法直接将当前的逻辑表对应到定义好的分库分表规则枚举,因此对 DbAndTableEnum进行遍历,这里用到了枚举的 values() 方法 ,对该方法的解析,放在了文章的 附录 中。
  2. 在每一轮循环中,我们将枚举规则中定义的逻辑表名与ShardingValue中的逻辑表名比较,相等表名路由是正确的,则继续比对路由ShardingValue中的路由键key与枚举中定义的key是否相等(如:order_id),相等则通过String.subString(int beginIndex, int endIndex)方法截取当前分片键值(如:OD000000011903261545475143200001)中的数据库的索引,注意去掉前面补位的0。结束循环并将该库索引返回
  3. 如果匹配逻辑表成功,匹配分片键失败,我们认为是使用了外部主键(如:使用用户user_id查询了订单信息)则通过取模方式进行取库下标操作。这样就同时支持了通过主键、外部id的方式进行查询。

开发自定义复合分表策略

完成了数据源的路由,我们接着实现对数据表的路由策略。

方法基本和实现数据源路由相同,也是要实现 ComplexKeysShardingAlgorithm 接口。需要重写其 doSharding(Collection availableTargetNames, Collection shardingValues) 方法。

区别在于分表策略的目的是选择物理表索引,最终告知Sharding-JDBC将数据发往分片的那个物理分表上。

建立复合分表策略SnoWalkerComplexShardingTB.java,代码实现如下:

@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, Collection<ShardingValue> shardingValues) {
    // 1. 打印物理分表集合 及 分片键属性集合
    log.info("availableTargetNames:" + JSON.toJSONString(availableTargetNames) + ",shardingValues:" + JSON.toJSONString(shardingValues));

    // availableTargetNames:["t_new_order_0000","t_new_order_0001"],
    // shardingValues:[{"columnName":"order_id","logicTableName":"t_new_order","values":["OD010001011903261549424993200011"]},{"columnName":"user_id","logicTableName":"t_new_order","values":["UD030001011903261549424973200007"]}]
    Collection<String> collection = new ArrayList<>();
    // 2. 遍历分片键集合
    for (ShardingValue var : shardingValues) {
        // 2.1 逻辑与分库逻辑相同,转换ShardingValue为ListShardingValue
        ListShardingValue<String> listShardingValue = (ListShardingValue<String>)var;
        List<String> shardingValue = (List<String>)listShardingValue.getValues();
        // 3. 打印当前分片键的真实值
        // shardingValue:["OD010001011903261549424993200011"]
        log.info("shardingValue:" + JSON.toJSONString(shardingValue));

        // 4. 根据分片键的真实值获取数据分表索引值
        String index = getIndex(listShardingValue.getLogicTableName(),                              listShardingValue.getColumnName(),
                                   shardingValue.get(0));
        // 5. 循环匹配数据表,通过String.endsWith(String suffix)
        // 判断第4步中获取到的索引是否包含在当前循环的物理分表中,
        // (如:判断t_new_order_0000中是否包含“_0000”)
        // 从而证明当前数据匹配物理分表成功。
        for (String availableTargetName : availableTargetNames) {
            if (availableTargetName.endsWith("_" + index)) {
                collection.add(availableTargetName);
                break;
            }
        }
        // 6. 只要匹配成功一种路由规则就退出
        if (collection.size() > 0) {
            break;
        }
    }
    // 7. 返回表路由结果
    return collection;
}   

核心逻辑解析

可以看到基本上和物理分片路由规则相似,区别在于此处是选取物理分表,注释中已经写得比较详细了,着重讲一下第四、五步。

第四步中,通过和之前物理分片取索引的相同算法取到分片键中的分表值,然后通过 String.endsWith(String suffix) 判断availableTargetName是否以 “_” + 分表索引值 结尾,如果是,则表明匹配物理分表成功,将该物理分表的完整表名(如:t_new_order_0000)添加到事先定义好的路由集合中,返回给Sharding-JDBC供其回调。

此处再贴一下getIndex方法,以便加深读者理解。

/**
 * 根据分片键计算分片节点
 * @param logicTableName
 * @param columnName
 * @param shardingValue
 * @return
 */
public String getIndex(String logicTableName,String columnName,String shardingValue) {
    String index = "";
    if (StringUtils.isBlank(shardingValue)) {
        throw new IllegalArgumentException("分片键值为空");
    }
    //截取分片键值-下标循环主键规则枚举类,匹配主键列名得到规则
    for (DbAndTableEnum targetEnum : DbAndTableEnum.values()) {
        //目标表路由
        if (targetEnum.getTableName().equals(logicTableName)) {
            //目标表的目标主键路由-例如:根据订单id查询订单信息
            if (targetEnum.getShardingKey().equals(columnName)) {
                index = getTbIndexBySubString(targetEnum, shardingValue);
            }else{
                //目标表的非目标主键路由-例如:根据内部用户id查询订单信息-内部用户id路由-固定取按照用户表库表数量
                //兼容且仅限根据外部id查询用户信息
                index = getTbIndexByMod(targetEnum, shardingValue);
            }
            break;
        }
    }
    if (StringUtils.isBlank(index)) {
        String msg = "从分片键值中解析表索引异常:logicTableName=" + logicTableName + "|columnName=" + columnName + "|shardingValue=" + shardingValue;
        throw new IllegalArgumentException(msg);
    }
    return index;
}

完整的代码请移步 snowalker-shardingjdbc-demo ,分库分表策略代码路径如下:

## 自定义分库策略实现类路径
com.snowalker.shardingjdbc.snowalker.demo.complex.sharding
    .strategy.SnoWalkerComplexShardingTB
## 自定义分表策略实现类路径
com.snowalker.shardingjdbc.snowalker.demo.complex.sharding
    .strategy.SnoWalkerComplexShardingDB

到这里,我们就完成了自定义复合分库分表策略的开发,接下来就写几个测试用例实际测试一下。

测试前置–配置自定义分库分表策略

这里以订单表t_new_order_0000 – t_new_order_0001的配置为例,在分片配置文件中添加如下配置项,将分库、分表策略指向我们定义好的策略类。

###############################################################
#
#                    shardingjdbc--分片规则--复合分片--订单表
#           根据user_id取模分库, 且根据order_id取模分表的两库两表的配置。
#
###############################################################
#订单表多分片键策略配置
sharding.jdbc.config.sharding.tables.t_new_order.actualDataNodes
    =ds$->{0..3}.t_new_order_000$->{0..1}
#指定分库分片键
sharding.jdbc.config.sharding.tables.t_new_order.databaseStrategy.complex.shardingColumns
    =user_id,order_id
#指定自定义分库策略类
sharding.jdbc.config.sharding.tables.t_new_order.databaseStrategy.complex.algorithmClassName
    =com.snowalker.shardingjdbc.snowalker.demo.complex.sharding.strategy.SnoWalkerComplexShardingDB
#指定分表分片键
sharding.jdbc.config.sharding.tables.t_new_order.tableStrategy.complex.shardingColumns
    =user_id,order_id
#指定自定义分表策略类
sharding.jdbc.config.sharding.tables.t_new_order.tableStrategy.complex.algorithmClassName
    =com.snowalker.shardingjdbc.snowalker.demo.complex.sharding.strategy.SnoWalkerComplexShardingTB

注意 指定分片实现类时,一定要使用全限定名。Sharding-JDBC会解析配置文件后帮我们加载自定义的分片策略。

测试用例A–新增订单数据

测试用例代码如下:

/**
 * 测试订单入库
 */
@Test
public void testNewOrderInsert() {
    // 支付宝或者微信uid
    String outId = "1232132131241241243126";
    LOGGER.info("获取id开始");
    String innerUserId = keyGenerator.generateKey(DbAndTableEnum.T_USER, outId);
    LOGGER.info("外部id={},内部用户={}", outId, innerUserId);
    String orderId = keyGenerator.generateKey(DbAndTableEnum.T_NEW_ORDER, innerUserId);
    LOGGER.info("外部id={},内部用户={},订单={}", outId, innerUserId, orderId);
    OrderNewInfoEntity orderInfo = new OrderNewInfoEntity();
    orderInfo.setUserName("snowalker");
    orderInfo.setUserId(innerUserId);
    orderInfo.setOrderId(orderId);
    orderNewSerivce.addOrder(orderInfo);

}

执行用例,日志打印如下:

2019-03-26 21:25:31.296  INFO 12996 --- [           main] nowalkerShardingjdbcDemoApplicationTests : 
    获取id开始
2019-03-26 21:25:34.755  INFO 12996 --- [           main] nowalkerShardingjdbcDemoApplicationTests : 
外部id=12321321312412412431260,内部用户=UD020000011903262125313013200040
2019-03-26 21:25:34.758  INFO 12996 --- [           main] nowalkerShardingjdbcDemoApplicationTests : 
外部id=12321321312412412431260,内部用户=UD020000011903262125313013200040,订单=OD000000011903262125347553200121
2019-03-26 21:25:34.758  INFO 12996 --- [           main] c.s.s.s.d.c.s.service.OrderNewSerivce    : 
订单入库开始,orderinfo=OrderNewInfoEntity{id=null, userId='UD020000011903262125313013200040', orderId='OD000000011903262125347553200121', userName='snowalker'}
2019-03-26 21:25:34.921  INFO 12996 --- [           main] s.s.s.d.c.s.s.SnoWalkerComplexShardingDB : 
availableTargetNames:["ds0","ds1","ds2","ds3"],shardingValues:[{"columnName":"user_id","logicTableName":"t_new_order","values":["UD020000011903262125313013200040"]},{"columnName":"order_id","logicTableName":"t_new_order","values":["OD000000011903262125347553200121"]}]
2019-03-26 21:25:34.921  INFO 12996 --- [           main] s.s.s.d.c.s.s.SnoWalkerComplexShardingDB : 
shardingValue:["UD020000011903262125313013200040"]
2019-03-26 21:25:34.921  INFO 12996 --- [           main] s.s.s.d.c.s.s.SnoWalkerComplexShardingTB : 
availableTargetNames:["t_new_order_0000","t_new_order_0001"],shardingValues:[{"columnName":"user_id","logicTableName":"t_new_order","values":["UD020000011903262125313013200040"]},{"columnName":"order_id","logicTableName":"t_new_order","values":["OD000000011903262125347553200121"]}]
2019-03-26 21:25:34.921  INFO 12996 --- [           main] s.s.s.d.c.s.s.SnoWalkerComplexShardingTB : 
shardingValue:["UD020000011903262125313013200040"]
2019-03-26 21:25:34.928  INFO 12996 --- [           main] Sharding-Sphere-SQL                      : Rule Type: sharding
2019-03-26 21:25:34.928  INFO 12996 --- [           main] Sharding-Sphere-SQL                      : 
    Logic SQL: insert into t_new_order(
            user_id,
            order_id,
            user_name
        )
        values
        (
            ?,
            ?,
            ?
        )
......
2019-03-26 21:25:34.928  INFO 12996 --- [           main] Sharding-Sphere-SQL                      : 
Actual SQL: ds0 ::: insert into t_new_order_0000(
        user_id,
        order_id,
        user_name
    )
    values
    (
        ?,
        ?,
        ?
    ) ::: [[UD020000011903262125313013200040, OD000000011903262125347553200121, snowalker]]

可以看到,我们生成的订单号OD000000011903262125347553200121被路由到ds0的t_new_order_0000表,这和我们的订单号中的第四位到第九位的 00(库)0000(表)吻合,表明我们的策略配合主键生成器生效了且是符合预期要求的。

测试用例B–通过订单号查询订单明细

测试用例代码如下:

/**
 * 测试订单明细查询
 */
@Test
public void testQueryNewOrderById() {
    String orderId = "OD010001011903261549424993200011";
    String userId = "UD030001011903261549424973200007";
    OrderNewInfoEntity orderInfo = new OrderNewInfoEntity();
    orderInfo.setOrderId(orderId);
    orderInfo.setUserId(userId);
    System.out.println(orderNewSerivce.queryOrderInfoByOrderId(orderInfo));
}

我们要查询订单号为OD010001011903261549424993200011,用户id=UD030001011903261549424973200007的订单明细,执行该测试用例,日志打印如下:

2019-03-26 21:32:16.459  INFO 16140 --- [           main] s.s.s.d.c.s.s.SnoWalkerComplexShardingDB : 
availableTargetNames:["ds0","ds1","ds2","ds3"],shardingValues:[{"columnName":"order_id","logicTableName":"t_new_order","values":["OD010001011903261549424993200011"]},{"columnName":"user_id","logicTableName":"t_new_order","values":["UD030001011903261549424973200007"]}]
2019-03-26 21:32:16.459  INFO 16140 --- [           main] s.s.s.d.c.s.s.SnoWalkerComplexShardingDB : 
shardingValue:["OD010001011903261549424993200011"]
2019-03-26 21:32:16.466  INFO 16140 --- [           main] s.s.s.d.c.s.s.SnoWalkerComplexShardingTB : 
availableTargetNames:["t_new_order_0000","t_new_order_0001"],shardingValues:[{"columnName":"order_id","logicTableName":"t_new_order","values":["OD010001011903261549424993200011"]},{"columnName":"user_id","logicTableName":"t_new_order","values":["UD030001011903261549424973200007"]}]
2019-03-26 21:32:16.466  INFO 16140 --- [           main] s.s.s.d.c.s.s.SnoWalkerComplexShardingTB : 
shardingValue:["OD010001011903261549424993200011"]
2019-03-26 21:32:16.474  INFO 16140 --- [           main] Sharding-Sphere-SQL                      : Rule Type: sharding
2019-03-26 21:32:16.475  INFO 16140 --- [           main] Sharding-Sphere-SQL                      : 
Logic SQL: select
            t.id as id,
            t.user_id as userId,
            t.order_id as orderId,
            t.user_name as userName
        from t_new_order t
        where t.order_id=?
        and t.user_id=?
......
2019-03-26 21:32:16.476  INFO 16140 --- [           main] Sharding-Sphere-SQL                      : 
Actual SQL: ds1 ::: select
            t.id as id,
            t.user_id as userId,
            t.order_id as orderId,
            t.user_name as userName
        from t_new_order_0001 t
        where t.order_id=?
        and t.user_id=? ::: [[OD010001011903261549424993200011, UD030001011903261549424973200007]]
OrderNewInfoEntity{id=1249, userId='UD030001011903261549424973200007', orderId='OD010001011903261549424993200011', userName='snowalker'}

sql中传入了两个查询条件,首先匹配到的是order_id,因此先按照order_id进行路由,找到ds1的t_new_order_0001表,在该表中执行真实查询SQL如下

select
    t.id as id,
    t.user_id as userId,
    t.order_id as orderId,
    t.user_name as userName
from t_new_order_0001 t
where t.order_id=?
and t.user_id=? ::: [[OD010001011903261549424993200011, UD030001011903261549424973200007]]

查询结果为

OrderNewInfoEntity{id=1249, 
    userId='UD030001011903261549424973200007', orderId='OD010001011903261549424993200011', 
    userName='snowalker'}

可以看到,依旧满足我们的要求,表明我们的复合路由策略是正确的。

小结

本文中,我们结合之前的自定义分布式主键,完成了Sharding-JDBC中复合分片路由算法的自定义实现,并经过测试验证符合预期。

该实现方案在生产上已经经历过考验,读者可以借鉴并运用到自己的项目中。

定义分片路由策略的核心还是要熟悉ComplexKeysShardingAlgorithm,对如何解析 doSharding(Collection availableTargetNames, Collection shardingValues)的参数有明确的认识,最简单的方法就是实际打印一下参数,相信会让你更加直观的感受到作者优良的接口设计能力,站在巨人的肩膀上我们能看到更远。

到此,跟我学Sharding-JDBC系列就告一段落,后续待笔者功力精进,会对该框架的源码做进一步的解析,让我们不见不散。

跟我学Sharding-JDBC源码github地址

附录:解析枚举类的values()方法

枚举的 values() 方法 是编译器生成的static方法,因此在Enum类中并没出现values()方法,它是编译器编译枚举类后添加的。values()方法的作用就是获取枚举类中的所有变量,并作为数组返回。

注意,由于values()方法是编译器插入到枚举类中的static方法,所以如果我们将枚举实例向上转型为Enum,则values()方法将无法被调用,因为Enum类中并没有values()方法,valueOf()方法也是同样的道理,注意是一个参数的。

参考链接:

深入理解Java枚举类型(enum)

文章目录
  1. 1. 了解Sharding-JDBC的数据分片策略
    1. 1.1. 精确分片算法–PreciseShardingAlgorithm
    2. 1.2. 范围分片算法–RangeShardingAlgorithm
    3. 1.3. 复合分片算法–ComplexKeysShardingAlgorithm
    4. 1.4. Hint分片算法–HintShardingAlgorithm
    5. 1.5. 标准分片策略–StandardShardingStrategy
    6. 1.6. 复合分片策略–ComplexShardingStrategy
    7. 1.7. 行表达式分片策略–InlineShardingStrategy
    8. 1.8. Hint分片策略–HintShardingStrategy
    9. 1.9. 不分片策略–NoneShardingStrategy
  2. 2. 实战–自定义复合分片策略
    1. 2.1. 场景回顾
    2. 2.2. 开发自定义复合分库策略
      1. 2.2.1. 核心逻辑
      2. 2.2.2. 核心逻辑解析
    3. 2.3. 开发自定义复合分表策略
      1. 2.3.1. 核心逻辑解析
    4. 2.4. 测试前置–配置自定义分库分表策略
    5. 2.5. 测试用例A–新增订单数据
    6. 2.6. 测试用例B–通过订单号查询订单明细
  3. 3. 小结
  4. 4. 附录:解析枚举类的values()方法
Fork me on GitHub