--- title: "07-技术精华-解锁分库分表新姿势:基因法完全解读" created: 2025-12-11 aliases: - 技术精华-解锁分库分表新姿势:基因法完全解读 tags: - 项目 --- # 技术精华-解锁分库分表新姿势:基因法完全解读 ![[image-ebc09e17.png]] ## 背景 在分库分表时,经常会遇到查询的条件不含有分片键的情况,比如说用户表,生成的订单中是依靠userId来关联用户信息,而用户在登录时又可以使用手机号和邮箱来登录,这样只有userId一个分片键就搞不定了。 大麦网中的解决方案是在设计出 用户手机表 用手机号当做分片键 。以及用户邮箱表 用邮箱当做分片键。 当用户用手机或者邮箱登录后,分别从相应的用户手机表和用户邮箱表查询出userId,然后用userId去用户表查询信息。 目前在订单业务中也遇到类似的问题,我们需要既可以通过订单号查询出订单详细,也想通过userId查询该用户下的订单列表,这样就需要即通过order\_number查询又要通过userId查询 看着这里,估计小伙伴就会想了,还是使用附属表路由的方案呗,再设计出一个订单用户表,通过userId去订单用户表查询,得到order\_number,然后再去订单表查询信息 首先说这种方案确实可以解决,但就是要额外维护表。而且对于订单这种量级很大的表来说,附属路由表的量级也会很大。 所以最好有另一种方案,可以不用再设计出一张表去维护它,这种方案就是我们要介绍的 基因法 ## 基因法 我们先介绍一下特征: ```text 191 % 32 = 31 ``` - 191的二进制: 10111111 - 32的二进制: 100000 也就是2的5次方 - 31的二进制: 11111 让我们观察下这个计算规律: 31的二进制 和 191的二进制最后5位相等,都是11111。如果随便拿一个数转成二进制,然后把二进制的后5位替换成这个11111,然后将这个替换后的值对32进行取模,那么得到的余数也是31,这个5位长度就是靠32的二进制的长度也就是求log2n对数的值 比如说 159,二进制是 10011111 ,对 32 取模,结果还是31 下面让我们把这个计算规律应用到订单号和用户id的业务中 ```java public class TestMain { public static void main(String[] args) { //分片数量 int shardingCount = 32; //订单编号 long orderNumber = 2654324532L; //用户id long userId = 45346343212L; //求shardingCount的log2N对数 int sequenceShift = log2N(shardingCount); long newOrderNumber = replaceBinaryBits(orderNumber, sequenceShift, (userId % shardingCount)); System.out.println("替换后订单号取模结果:" + (newOrderNumber % shardingCount)); System.out.println("用户id取模结果:" + (userId % shardingCount)); } /** * 求log2(N) * */ public static int log2N(int count) { return (int)(Math.log(count)/ Math.log(2)); } public static long replaceBinaryBits(long num1, int numBits, long num2) { // 将两个数转换成二进制字符串 String binaryStr1 = Long.toBinaryString(num1); // 计算num2对应的二进制长度,确保它填充到指定的numBits位 String binaryStr2 = Long.toBinaryString(num2 % (1L << numBits)); // 如果binaryStr2长度小于numBits,则在前面补零 while (binaryStr2.length() < numBits) { binaryStr2 = "0" + binaryStr2; } // 计算需要保留的前面部分的长度 int keepLength = binaryStr1.length() - numBits; // 如果需要替换的位数超过了num1的长度,则直接使用num2的二进制字符串 if(keepLength < 0) { System.out.println("替换位数超过了第一个参数的位数,直接使用第三个参数的二进制: " + binaryStr2); // 直接将num2的二进制字符串转换为十进制并打印 return Long.parseLong(binaryStr2, 2); } // 保留num1前面的部分和num2的二进制拼接 String resultBinaryStr = binaryStr1.substring(0, keepLength) + binaryStr2; System.out.println("要替换的二进制后位数" + numBits); // 打印原始的二进制字符串 System.out.println("num1 替换前的二进制: " + binaryStr1); // 打印最终的二进制字符串 System.out.println("num1 替换后的二进制: " + resultBinaryStr); System.out.println("num2 的十进制: " + num2); System.out.println("num2 的二进制: " + binaryStr2); // 将结果二进制字符串转换回十进制数值并打印 long resultDecimal = Long.parseLong(resultBinaryStr, 2); System.out.println("num1 对应的十进制数为: " + resultDecimal); return resultDecimal; } } ``` 结果: ```text 要替换的二进制后位数5 num1 替换前的二进制: 10011110001101011100011100110100 num1 替换后的二进制: 10011110001101011100011100101100 num2 的十进制: 12 num2 的二进制: 01100 num1 对应的十进制数为: 2654324524 替换后订单号取模结果:12 用户id取模结果:12 ``` 小伙伴可能没有看懂流程,本人来总结下: - 假设 分片数量shardingCount = 32,订单号orderNumber = 2654324532L,用户iduserId = 45346343212L,分片数量32的log2n对数sequenceShift = 5 - 将orderNumber转为二进制,为10011110001101011100011100110100 - 将userId % shardingCount的余数 = 12 转为二进制1100 - 根据sequenceShift长度是5,把 1100补成01100 - 将10011110001101011100011100110100的后5位替换成01100,替换后为10011110001101011100011100101100 - 将替换后的10011110001101011100011100101100转为十进制为2654324524 - 计算2654324524 % 32 = 12 看到这相信大家已经明白基因法的原理了,下面来介绍如何具体的应用 现在分布式id的生成基本用的都是雪花算法的原理,我们再来看一下雪花算法的结构 ### 雪花算法 ![[image-fde55240.png]] - 第一个部分:1个bit,无意义,固定为0。二进制中最高位是符号位,1表示负数,0表示正数。ID都是正整数,所以固定为0。 - 第二个部分:41个bit,表示时间戳,精确到毫秒,2^41/(1000606024365)=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个订单才能重复,这要是正常的用户肯定是不会重复的,除非是机器刷单或者恶意攻击这种情况 ### 改造 下面我们来对雪花算法进行改造,结合基因法来测试一下重复的情况 ```java public synchronized long getOrderNumber(long userId) { long timestamp = getBase(); // 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分 return ((timestamp - BASIS_TIME) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence << 4 | userId % 16; } ``` - userId的取模为 16,对其去log2n的对数是4 - 将序列号左移4位,也就是序列号的有效位是8位,最高支持到2的8次方 256,也就是说某个用户在毫秒内,同一机器下最多生成256个订单号不会重复 接下来下面我们来验证一下 循环256次 ```java public void testOrderNumber(){ List idList = new ArrayList<>(); for (int i = 0; i < 256; i++) { long orderNumber = snowflakeIdGenerator.getOrderNumber(2222); idList.add(orderNumber); } int count = 0; Map> collect = idList.stream().collect(Collectors.groupingBy(id -> id)); for (final Entry> entry : collect.entrySet()) { System.out.println("订单号:" + entry.getKey() + ",数量为:" + entry.getValue().size()); if (entry.getValue().size() > 1) { count++; } } System.out.println("重复的orderNumber数量:" + count); } ``` 结果 ```text .... 订单号:1763164230291425342,数量为:1 订单号:1763164230291425854,数量为:1 重复的orderNumber数量:0 ``` 下面来做进一步的改造,可根据userId和要配置的分表数量来灵活的生成订单编号 com.damai.toolkit.SnowflakeIdGenerator#getOrderNumber(long, long) ```java /** * 获取订单编号 * * @return orderNumber */ public synchronized long getOrderNumber(long userId,long tableCount) { long timestamp = getBase(); long sequenceShift = log2N(tableCount); // 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分 | 用户id基因 return ((timestamp - BASIS_TIME) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | (sequence << sequenceShift) | (userId % tableCount); } ``` - 需要userId 和tableCount分片数量作为参数传入 - 根据tableCount计算出log2n的对数sequenceShift - 将序列号左移sequenceShift位 - 将序列号左移sequenceShift位后,剩下的位数替换成userId % tableCount的基因 - 拼接好数据后将id返回,作为订单编号 通过上述可知,毫秒内能生成不重复的订单号和分片数量成相反关系,根据业务需求进行相关的调整即可,目前的订单业务是由通过订单编号和用户id查询,可以用基因法,但如果查询的条件变多了,比如查询某个节目类型下的订单列表,或者统计数量,那么用基因法就不适合了,还是要考虑使用附属路由表的方案,或者使用其他类型数据库,比如elasticsearch,而在节目服务服务的数据查询中,就使用到了elasticsearch,小伙伴也直接跳转到节目服务的相关业务介绍模块即可 另外,关于雪花算法的详细介绍和组件使用,小伙伴可跳转到相关文档 **技术精华-雪花算法完全解读** 关于根据基因法改造后的生成订单编号如果使用,可查看分布式id生成器的介绍部分 **组件讲解-分布式ID生成器揭秘,保障数据唯一性的核心组件** --- **企业级项目导航**:⬅️ [[06-分库分表-支付服务|06-分库分表-支付服务]] | 07-技术精华-解锁分库分表新姿势:基因法完全解读 | ➡️ [[00-业务深读|00-业务深读]]