Skip to content

技术精华-解锁分库分表新姿势:基因法完全解读(优化后的方案)

注意

优化后基因法和后续的扩容代码都在大麦pro中了,大麦普通版还是之前旧的方案。

所以大家要去申请大麦pro的项目,详细的申请步骤:

大麦pro版本功能介绍

背景

在分库分表时,经常会遇到查询的条件不含有分片键的情况,比如说用户表,生成的订单中是依靠userId来关联用户信息,而用户在登录时又可以使用手机号和邮箱来登录,这样只有userId一个分片键就搞不定了。

大麦网中的解决方案是在设计出 用户手机表 用手机号当做分片键 。以及用户邮箱表 用邮箱当做分片键。 当用户用手机或者邮箱登录后,分别从相应的用户手机表和用户邮箱表查询出userId,然后用userId去用户表查询信息。

目前在订单业务中也遇到类似的问题,我们需要既可以通过订单号查询出订单详细,也想通过userId查询该用户下的订单列表,这样就需要即通过order_number查询又要通过userId查询

看着这里,估计小伙伴就会想了,还是使用附属表路由的方案呗,再设计出一个订单用户表,通过userId去订单用户表查询,得到order_number,然后再去订单表查询信息

首先说这种方案确实可以解决,但就是要额外维护表。而且对于订单这种量级很大的表来说,附属路由表的量级也会很大。

所以最好有另一种方案,可以不用再设计出一张表去维护它,这种方案就是我们要介绍的 基因法

现在分布式id的生成基本用的都是雪花算法的原理,我们再来看一下雪花算法的结构

雪花算法

%E9%9B%AA%E8%8A%B1%E7%AE%97%E6%B3%95.png

  • 第一个部分: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查,都能路由到同一个库、同一张表。

plain
订单号结构:[时间戳][数据中心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)

java
/**
 * 【方案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;
}

执行流程图解

plain
┌─────────────────────────────────────────────────────────────┐
│                    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的二进制

plain
userId = 37
二进制 = 100101
后6位 = 100101 (就是37)

第2步:提取基因

java
geneMask = 0b111111 = 63
userGene = 37 & 63 = 37

第3步:组装订单号

假设当前时间戳差值、数据中心ID、机器ID、序列号如下:

plain
时间戳差值 = 1234567890
数据中心ID = 1
机器ID = 2
序列号 = 100

计算过程:

plain
订单号 = (1234567890 << 22) | (1 << 17) | (2 << 12) | (100 << 6) | 37
       = 5178139721474048 | 131072 | 8192 | 6400 | 37
       = 5178139721619749

第4步:验证基因

plain
订单号 = 5178139721619749
订单号 & 63 = 5178139721619749 & 63 = 37
✅ 后6位就是 userId 的后6位!

订单号结构位图

plain
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的基因一致性

plain
userId    = xxxxxxxxxx100101
                      └──┬──┘
                      后6位 = 37

orderNumber = yyyyyyyyy100101
                       └──┬──┘
                       后6位 = 37

两者后6位完全相同!

2. 分库分表路由一致性

无论用哪个字段查询,都能路由到同一位置:

sql
-- 用订单号查
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生成器的介绍部分

组件讲解-分布式ID生成器揭秘,保障数据唯一性的核心组件

更新: 2026-02-07 16:58:47
原文: https://www.yuque.com/u22210564/ykdrdh/gddfucig5xr2dh3z

Java 后端面试知识库