前言:前面我们对比过雪花算法与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字节做哈希再取模,打散流量。
重点区分两个概念:
- 主键:UUIDv7(BINARY(16)),保证全局唯一、有序写入;
- 分片键:复用这个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底层实现可能变化,升级版本后分片路由发生漂移,数据找不到,这也是我们手写自定义算法的核心原因------哈希逻辑完全由业务掌控,版本升级无风险。
七、选型总结
✅ 适合场景:
- 主键必须全局唯一、希望InnoDB写入有序,避免索引页分裂;
- 查询模式以主键等值查询、IN查询为主;
- 希望流量均匀打散到各个分片,规避时序主键带来的分片热点。
❌ 不适合场景:
- 核心业务大量按主键时间范围检索;
- 需要频繁增减分片、不想做数据迁移。
八、延伸拓展
- 替换MurmurHash3优化哈希性能,高吞吐场景压测对比SHA256;
- 改造为一致性哈希版本,支持分片平滑扩容;
- 绑定表、读写分离和这套UUIDv7分片一起组合使用;
- Proxy模式下使用自定义算法:需要打包算法jar放到ShardingSphere-Proxy的ext-lib目录。