ShardingSphere 自定义分片算法:BINARY(16) UUIDv7 哈希分片实现

前言:前面我们对比过雪花算法与UUIDv7在分库分表下的优劣,UUIDv7自带时间有序、全局唯一,非常适合做分布式主键;但很多同学踩坑:UUIDv7存为BINARY(16)字节数组,ShardingSphere内置HASH_MOD直接使用时行为不符合预期 。内置哈希分片大多针对数值类型,原生对byte\[\]、BINARY类型支持容易出现分布不均、类型转换异常。本文带你手写一套适配BINARY(16) UUIDv7的自定义哈希分片算法,同时配套建表、YAML配置、路由验证、扩容注意事项。

一、先理清核心背景

UUIDv7遵循RFC9562,128bit结构:高位是毫秒时间戳,低位是随机序列。

  • 推荐存储:BINARY(16),而不是CHAR(36)字符串,节省存储空间、主键索引体积更小、InnoDB聚簇索引写入有序,避免UUIDv4随机写入造成大量页分裂。
  • 痛点:BINARY(16)在JDBC中读到的是byte[],ShardingSphere自带HASH_MOD分片算法直接传入byte数组,哈希逻辑不是我们可控的;并且UUIDv7前缀是时间,不能直接拿时间段分片(会造成新数据全部打在同一个分片热点),所以需要对整个16字节做哈希再取模,打散流量。

重点区分两个概念:

  1. 主键:UUIDv7(BINARY(16)),保证全局唯一、有序写入;
  2. 分片键:复用这个UUIDv7主键,对16字节整体哈希后取模分片,打散时序带来的热点。

二、MySQL建表SQL示例

逻辑表 t_order,分片键为id,类型BINARY(16)。

复制代码
-- 单分片建表语句,分库分表后真实表为 t_order_0 ~ t_order_3
CREATE TABLE t_order (
    id BINARY(16) PRIMARY KEY COMMENT 'UUIDv7主键,分片键',
    order_no VARCHAR(64) NOT NULL,
    amount DECIMAL(18,2),
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

小提示:Java侧生成UUIDv7后,直接转byte\[\]写入BINARY(16);查询展示时,再把byte\[\]转成标准UUID字符串。

三、自定义分片算法代码实现(ShardingSphere 5.x)

ShardingSphere自定义标准分片算法,需要实现 StandardShardingAlgorithm<byte[]>,重写精确分片doSharding、范围分片doSharding(BETWEEN场景)。

路由逻辑:对16字节byte数组做SHA-256哈希,取哈希结果的long值,对分片数量取模,算出目标分片名称。你也可以替换为MurmurHash,性能更高。

复制代码
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;

import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.util.Collection;
import java.util.Collections;
import java.util.Properties;

public class UUIDv7BinaryHashShardingAlgorithm implements StandardShardingAlgorithm<byte[]> {

    private static final String SHARDING_COUNT_KEY = "sharding-count";
    private int shardingCount;

    @Override
    public void init(Properties props) {
        this.shardingCount = Integer.parseInt(props.getProperty(SHARDING_COUNT_KEY));
    }

    /**
     * = / IN 精确路由(核心)
     */
    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<byte[]> shardingValue) {
        byte[] uuidBytes = shardingValue.getValue();
        long hashVal = hashBytesToLong(uuidBytes);
        long idx = Math.abs(hashVal) % shardingCount;
        String suffix = String.valueOf(idx);
        // 匹配 t_order_0、t_order_1 ...
        return availableTargetNames.stream()
                .filter(name -> name.endsWith("_" + suffix))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("分片找不到,idx:" + idx));
    }

    /**
     * BETWEEN AND 范围查询:UUIDv7范围查询无法精准路由,返回全部分片
     */
    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<byte[]> shardingValue) {
        // UUIDv7按时间范围查,哈希分片无法定位分片,广播查询所有分片
        return availableTargetNames;
    }

    /**
     * 把16字节UUIDv7做SHA256,取前8字节转为long用于取模
     */
    private long hashBytesToLong(byte[] bytes) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(bytes);
            ByteBuffer buffer = ByteBuffer.wrap(hash, 0, 8);
            return buffer.getLong();
        } catch (Exception e) {
            throw new RuntimeException("UUIDv7哈希失败", e);
        }
    }

    @Override
    public Properties getProps() {
        Properties props = new Properties();
        props.setProperty(SHARDING_COUNT_KEY, String.valueOf(shardingCount));
        return props;
    }

    @Override
    public String getType() {
        // 算法标识,YAML配置里可以用这个type,或者直接CLASS_BASED指定全类名
        return "UUIDV7_BINARY_HASH";
    }
}

性能优化建议:SHA256开销略高,生产推荐替换MurmurHash3,纯字节哈希,CPU开销更低,适合高吞吐写入。

四、SpringBoot + ShardingSphere YAML配置

采用CLASS_BASED方式加载自定义分片算法,分表演示:4张分片表 t_order_0 ~ t_order_3,不分库,仅分表。

复制代码
spring:
  shardingsphere:
    props:
      sql-show: true
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds0.t_order_$->{0..3}
            table-strategy:
              standard:
                sharding-column: id
                sharding-algorithm-name: uuidv7-binary-hash
        sharding-algorithms:
          uuidv7-binary-hash:
            type: CLASS_BASED
            props:
              strategy: STANDARD
              algorithm-class: com.xxx.sharding.UUIDv7BinaryHashShardingAlgorithm
              sharding-count: 4
        key-generators:
          uuidv7-generator:
            type: CLASS_BASED
            props:
              algorithm-class: com.xxx.generator.UUIDv7Generator

补充:UUIDv7Generator 是你自己实现的主键生成器,输出byte\[\],写入BINARY(16)字段;不要用字符串UUID再转,避免两次转换。

五、路由验证 & 测试SQL

插入测试:

复制代码
INSERT INTO t_order(id,order_no,amount) VALUES (?, 'NO20261021001', 99.00);

参数是byte\[\]类型,ShardingSphere会交给自定义算法,计算哈希、取模,自动路由到对应真实分片。

查询测试:

复制代码
-- 等值查询:精准路由到单个分片,性能最好
SELECT * FROM t_order WHERE id = ?;

-- IN查询:路由到对应若干分片
SELECT * FROM t_order WHERE id IN (?,?);

-- 按create_time时间范围查询:哈希分片无法根据时间定位分片,广播查询4个分片,业务侧注意控制
SELECT * FROM t_order WHERE create_time > '2026-10-01';

六、关键坑点(重点)

坑1:不要直接用UUIDv7时间戳部分分片

UUIDv7高6字节是毫秒时间,如果直接拿时间做分片,新写入数据全部落在同一个分片,出现分片热点,失去分片均衡的意义。哈希打散是为了解决这个问题,保留有序主键优势,同时流量均匀。

坑2:BINARY(16)和字符串UUID混用,类型不匹配路由失效

很多业务代码:数据库BINARY(16),查询时传入去掉横杠的字符串hex,ShardingSphere拿到的分片值类型是String,和算法预期byte[]不匹配,路由错乱。

✅ 规范:Java层统一用byte\[\]传递分片键;展示层单独转字符串。

坑3:扩容重平衡麻烦(哈希分片通用短板)

固定取模哈希分片,分片数量变更时,大部分数据分片路由会变化,需要数据迁移。

方案选型参考:

  • 流量稳定、短期不扩容:当前哈希方案足够简单好用;
  • 未来需要平滑扩容:改成一致性哈希自定义算法。

坑4:范围查询会全分片广播

哈希分片天然不适合范围路由,UUIDv7虽然有序,但哈希之后顺序丢失。如果大量业务是按时间范围检索,不要把UUIDv7同时当做分片键,需要单独设计分片键(比如user_id)。

坑5:内置HASH_MOD对byte\[\]行为不可控

内置HASH_MOD对byte数组的哈希实现不是标准化的,不同版本ShardingSphere底层实现可能变化,升级版本后分片路由发生漂移,数据找不到,这也是我们手写自定义算法的核心原因------哈希逻辑完全由业务掌控,版本升级无风险。

七、选型总结

✅ 适合场景:

  1. 主键必须全局唯一、希望InnoDB写入有序,避免索引页分裂;
  2. 查询模式以主键等值查询、IN查询为主;
  3. 希望流量均匀打散到各个分片,规避时序主键带来的分片热点。

❌ 不适合场景:

  1. 核心业务大量按主键时间范围检索;
  2. 需要频繁增减分片、不想做数据迁移。

八、延伸拓展

  1. 替换MurmurHash3优化哈希性能,高吞吐场景压测对比SHA256;
  2. 改造为一致性哈希版本,支持分片平滑扩容;
  3. 绑定表、读写分离和这套UUIDv7分片一起组合使用;
  4. Proxy模式下使用自定义算法:需要打包算法jar放到ShardingSphere-Proxy的ext-lib目录。
相关推荐
罗湖老棍子12 小时前
【例 1】Sightseeing Trip(信息学奥赛一本通- P1494)(洛谷-P10927)
数据结构·算法·图论·最短路·floyd·最小环
雪碧聊技术12 小时前
算法基础—>散列表(哈希)—>线性探测的开放定址法解决冲突(顺序地探查可用存储单元)
哈希算法·散列表·线性探测的开放定址法
stsdddd14 小时前
YOLO算法建筑工地安全帽目标检测数据集16881张:标注核对笔记
算法·yolo·目标检测
CV爱数码15 小时前
论文精读|Learned Primal-Dual:把原始–对偶算法展开成网络
图像处理·算法·计算机视觉·ct·重建·断层扫描
yuniko-n15 小时前
【算法】约瑟夫环问题
java·算法
l1t16 小时前
测试DuckDB v2.1.0-alpha49002的bignum乘法
数据库·sql·算法·duckdb
Nil20816 小时前
LeetCode 283移动零
数据结构·算法·leetcode
CQU_JIAKE16 小时前
10.18【A】
算法
LMT流氓兔16 小时前
第五单元——第4课:给工具参数加校验
java·前端·算法·ai·langchain