技术精华-解锁分库分表新姿势:基因法完全解读(优化后的方案)
注意
优化后基因法和后续的扩容代码都在大麦pro中了,大麦普通版还是之前旧的方案。
所以大家要去申请大麦pro的项目,详细的申请步骤:
背景
在分库分表时,经常会遇到查询的条件不含有分片键的情况,比如说用户表,生成的订单中是依靠userId来关联用户信息,而用户在登录时又可以使用手机号和邮箱来登录,这样只有userId一个分片键就搞不定了。
大麦网中的解决方案是在设计出 用户手机表 用手机号当做分片键 。以及用户邮箱表 用邮箱当做分片键。 当用户用手机或者邮箱登录后,分别从相应的用户手机表和用户邮箱表查询出userId,然后用userId去用户表查询信息。
目前在订单业务中也遇到类似的问题,我们需要既可以通过订单号查询出订单详细,也想通过userId查询该用户下的订单列表,这样就需要即通过order_number查询又要通过userId查询
看着这里,估计小伙伴就会想了,还是使用附属表路由的方案呗,再设计出一个订单用户表,通过userId去订单用户表查询,得到order_number,然后再去订单表查询信息
首先说这种方案确实可以解决,但就是要额外维护表。而且对于订单这种量级很大的表来说,附属路由表的量级也会很大。
所以最好有另一种方案,可以不用再设计出一张表去维护它,这种方案就是我们要介绍的 基因法
现在分布式id的生成基本用的都是雪花算法的原理,我们再来看一下雪花算法的结构
雪花算法

- 第一个部分:1个bit,无意义,固定为0。二进制中最高位是符号位,1表示负数,0表示正数。ID都是正整数,所以固定为0。
- 第二个部分:41个bit,表示时间戳,精确到毫秒,2^41/(1000_60_60_24_365)=69,大概可以使用 69 年。时间戳带有自增属性。
- 第三个部分:10个bit,表示10位的机器标识,最多支持2^10=1024个节点。此部分也可拆分成5位datacenterId和5位workerId,datacenterId表示机房ID,workerId表示机器ID。
- 第四部分:12个bit,表示序列化,同一毫秒时间戳时,通过这个递增的序列号来区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成 2^12=4096 个不重复 id。
假设存在一种情况,在超高的并发下,在同一毫秒,同一台机器,生成两个id,那么这两个id唯一的区别 就是序列号相差1,如果这时我们使用了基因法,分成32张表,也就是取把雪花算法二进制的后5位进行基因替换,那么这两个id不就重复了吗?
确实是会重复但我们要和业务结合起来思考,如果是在订单业务下,**订单id被替换的是用户id和分片数的取模值,**在这种业务下发生重复的概率是非常低的,也就是说需要用户在同一毫秒下创建了2个订单才能重复,这要是正常的用户肯定是不会重复的,除非是机器刷单或者恶意攻击这种情况
核心思想
基因法的核心就是:把用户ID的后几位"嵌入"到订单号里,这样不管你用订单号查还是用用户ID查,都能路由到同一个库、同一张表。
订单号结构:[时间戳][数据中心ID][机器ID][序列号][userId后6位]
↑
这就是"基因"为什么固定6位基因?
| 基因位数 | 支持的分片组合 | 适用场景 |
|---|---|---|
| 3位 | 8种(2库×4表) | 当前配置 |
| 4位 | 16种(4库×4表 或 2库×8表) | 小规模扩容 |
| 6位 | 64种(8库×8表) | 设计上限 |
固定6位的好处:扩容时不需要改订单号生成代码,只改分片配置就行。
完整代码
**位置:**com.damai.toolkit.SnowflakeIdGenerator#getOrderNumber(long)
/**
* 【方案1】生成订单编号 - 固定预留6位基因位
* 核心思想:预留足够多的基因位,支持未来扩容而无需修改生成逻辑
*
* 基因位分配(6位可支持64种组合):
* - 当前:2库4表 = 8种组合,占用3位
* - 最大支持:8库 × 8表 = 64种组合
*
* 订单号结构:[时间戳][数据中心ID][机器ID][序列号][userId后6位]
*
* 扩容时只需修改分片算法配置,无需修改此方法
*
* @param userId 用户ID
* @return 订单编号
*/
public synchronized long getOrderNumber(long userId) {
long timestamp = getBase();
// 固定预留6位基因位,支持未来扩容
// 6位 = 可支持最大 2^6 = 64 种分片组合(8库8表)
long fixedGeneLength = 6L;
// 创建基因掩码:0b111111 (6个1)
long geneMask = (1L << fixedGeneLength) - 1;
// 从用户ID中提取后6位作为基因
long userGene = userId & geneMask;
// 生成订单编号
// 结构:[时间戳][数据中心ID][机器ID][序列号左移6位][基因6位]
return ((timestamp - BASIS_TIME) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| (sequence << fixedGeneLength)
| userGene;
}执行流程图解
┌─────────────────────────────────────────────────────────────┐
│ getOrderNumber(userId) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第1步:获取时间戳 │
│ timestamp = getBase() │
│ 处理时钟回拨、序列号自增等 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第2步:固定基因位数 = 6 │
│ fixedGeneLength = 6L │
│ 支持最大 8库×8表 = 64种组合 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第3步:创建基因掩码 │
│ geneMask = (1L << 6) - 1 = 0b111111 = 63 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第4步:提取用户ID的后6位 │
│ userGene = userId & geneMask │
│ │
│ 示例:userId = 37 (二进制: 100101) │
│ userGene = 37 & 63 = 37 (后6位: 100101) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第5步:组装订单号 │
│ │
│ [时间戳差值 << 22] | [数据中心ID << 17] | [机器ID << 12] │
│ | [序列号 << 6] | [基因6位] │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 返回订单号 │
└─────────────────────────────────────────────────────────────┘真实数据示例
场景:userId = 37 生成订单号
第1步:分解userId的二进制
userId = 37
二进制 = 100101
后6位 = 100101 (就是37)第2步:提取基因
geneMask = 0b111111 = 63
userGene = 37 & 63 = 37第3步:组装订单号
假设当前时间戳差值、数据中心ID、机器ID、序列号如下:
时间戳差值 = 1234567890
数据中心ID = 1
机器ID = 2
序列号 = 100计算过程:
订单号 = (1234567890 << 22) | (1 << 17) | (2 << 12) | (100 << 6) | 37
= 5178139721474048 | 131072 | 8192 | 6400 | 37
= 5178139721619749第4步:验证基因
订单号 = 5178139721619749
订单号 & 63 = 5178139721619749 & 63 = 37
✅ 后6位就是 userId 的后6位!订单号结构位图
64位订单号的bit分布:
63 41 40 36 35 31 30 18 17 12 11 6 5 0
┌─────────────────┬────────┬────────┬────────┬────────┬─────────┬─────────┐
│ 时间戳差值 │ 数据 │ 机器 │ 序 │ 序列 │ 序列号 │ 基因 │
│ (41 bits) │中心ID │ ID │ 列号 │ 号 │ 左移6位 │ (6bits) │
│ │(5bits) │(5bits) │ │ │ │ │
└─────────────────┴────────┴────────┴────────┴────────┴─────────┴─────────┘
↑
userId后6位嵌入这里
序列号被左移6位,为基因腾出空间七、核心特性
1. 订单号与userId的基因一致性
userId = xxxxxxxxxx100101
└──┬──┘
后6位 = 37
orderNumber = yyyyyyyyy100101
└──┬──┘
后6位 = 37
两者后6位完全相同!2. 分库分表路由一致性
无论用哪个字段查询,都能路由到同一位置:
-- 用订单号查
SELECT * FROM d_order WHERE order_number = 5178139721619749;
-- 路由计算:5178139721619749 的后6位 = 37
-- 用用户ID查
SELECT * FROM d_order WHERE user_id = 37;
-- 路由计算:37 的后6位 = 37
-- 两者后6位相同,所以路由到同一个库表!扩容支持
固定6位基因的设计,天然支持从小规模扩容到大规模:
| 扩容路径 | 使用的基因位 | 是否需要改代码 |
|---|---|---|
| 2库4表 → 2库8表 | 3位 → 4位 | ❌ 不需要 |
| 2库8表 → 4库8表 | 4位 → 5位 | ❌ 不需要 |
| 4库8表 → 8库8表 | 5位 → 6位 | ❌ 不需要 |
因为订单号始终保留了完整的6位基因,扩容只需要改分片配置!
通过上述可知,毫秒内能生成不重复的订单号和分片数量成相反关系,根据业务需求进行相关的调整即可,目前的订单业务是由通过订单编号和用户id查询,可以用基因法,但如果查询的条件变多了,比如查询某个节目类型下的订单列表,或者统计数量,那么用基因法就不适合了,还是要考虑使用附属路由表的方案,或者使用其他类型数据库,比如elasticsearch,而在节目服务服务的数据查询中,就使用到了elasticsearch,小伙伴也直接跳转到节目服务的相关业务介绍模块即可
另外,关于雪花算法的详细介绍和组件使用,小伙伴可跳转到相关文档
关于根据基因法改造后的生成订单编号如果使用,可查看分布式id生成器的介绍部分
更新: 2026-02-07 16:58:47
原文: https://www.yuque.com/u22210564/ykdrdh/gddfucig5xr2dh3z