在 64 位的世界里,最左边的那一位,决定了一切。
一、背景
在测试环境中,发现数据库中的 ID 值出现负数现象,第一反应是 ID 超过 long 类型能表示的最大值,导致数据转成负值。
Java 中 long 类型的最大值为 2^63 - 1 = 9223372036854775807,超过该值后会转成负值。
二、实际环境分析数据
生产环境数据分析:
- 按最近3个月 ID 增长趋势预估:预测 5.5 个月后会增长至 long 最大值
- 按最近16个月 ID 增长趋势预估:预测仅2+个月后会增长至 long 最大值
可见 ID 增长速度有加快趋势,如果未采取处理举措,将很快遇到全量 ID 转成负值问题。
三、负 ID 对系统的影响
1. 数据库影响
- 性能问题:ID 递增类值用作主键时,如果达到负值,会导致类 B+ 树结构非最后端插入,触发频繁的页分裂,降低性能
- 排序问题:负 ID 会导致排序结果异常,尤其是用 ID 进行排序的场景
2. 业务影响
- 分页查询崩溃:一些场景依赖 ID 递增,实现分页(如每次按 ID 条件查询大于上次最大 ID ),不支持负值时会导致查询失效或重复数据
四、解决方案
业务层面(代码侵入)
id 转成BigInteger 然后数据库用无符号bigint
百度id uid generator 方向
默认配置
1 | protected int timeBits = 28; |
配置解析
| timeBits (28) | workerBits (22) | seqBits (13) |
|---|---|---|
| 时间戳(秒) | 机器号(固定为29) | 每秒序列号 |
UID = (deltaSeconds << (22 + 13)) | (workerId << 13) | sequence;
序列号(sequence)
2^13 - 1 = 8191
- 最多能表示:8192 个唯一值
- 范围是:0 ~ 8191(共 8192 个)
- 所以在十进制里:最后最多就是 8191
机器号(workerId)
2^22 - 1 = 4,194,304
- 最多能表示:4,194,303个唯一值
- 范围是:0 ~ 4,194,303(共 4,194,304个)
- 所以在十进制里:最后最多就是 4,194,304
时间(deltaSeconds )
2^28 - 1 = 268,435,455 秒
- 秒:268,435,455 秒
- 分钟:≈ 4,473,924 分钟
- 小时:≈ 74,565 小时
- 天数:≈ 3,107 天
- 年数:≈ 8.5 年
你从设置的起始时间(Epoch)开始,最多能支撑 8.5 年内的 UID 生成,
问题原因
Epoch默认时间 :2016-05-20
2016+8.5年 2025年之后会超变成负数
处理方案
目标 :
保证id 不重复
尽量保证id 递增趋势
当前id :9090949948782127403L
0111111000101001100010101101000000000000000000111010100100101011(64位)
0 标志位
1111110001010011000101011010 → 264581466 (时间差秒)
0000000000000000011101 → 29 (机器号)
0100100101011 → 2347 (序列号)
处理方法 : 调整uid 各组成位置位数
首先保证id 不重复可以通过修改机器号,不使用之前的机器id 生成uid 可以保证id 不与之前重复,即使时间戳一样
将时间差位置扩充以便能使用的时间更长,减少机器号位置
30位 ≈ 31, 557,600 秒 ≈ 34.0年 机器号:20位=1,048,576个
1
2
3protected int timeBits = 30;
protected int workerBits = 20;
protected int seqBits = 13;UID = (deltaSeconds << (20 + 13)) | (workerId << 13) | sequence;
只将时间戳位置 扩充30位,如果时间差还是当前时间差,那时间戳位置向后移动两位,生成的id 必然小于之前id,观察目前2进制时间差前6位都是1 如果想生成的id大于之前id,需要保证 标识位0 后 7位都是1 ,这样 时间戳位置就需要扩充至37位 ,前 7位为1 保证大于之前id 但需要每次算时间差 + 上前7为是1后30位为0的37位2进制数(1111111000000000000000000000000000000 =34091302913),但同时机器位缩小至13位 只能表示8192台机器号 (如果机器号不够用可以折中,适当缩小时间差位数,但可能生成id 会小于之前id 位数缩短越多交叉越多)
1
2
3protected int timeBits = 37;
protected int workerBits = 13;
protected int seqBits = 13;UID = (mask << 30+13 + 13) | (deltaSeconds << (13 + 13)) | (workerId << 13) | sequence;
处理流程及源码修改
公式可能不好理,解通俗解释:
28位二进制时间差 + 22位二进制机器号 + 13位 二进制序列号
举例:
- 时间 2025-04-16 00:00:00 时间戳 1744732800 秒
- Epoch默认时间 :2016-05-20 00:00:00 时间戳 1463673600秒
- 时间差 281059200 = 1744732800 - 1463673600
- 机器号比如 现在是 29 序列号1024
- id = 二进制(时间差 281059200) + 二进制(29)+ 二进制(1024) 不够需要补全对应位置的位数
- 10000110000001001111110000000 + 0000000000000000011101 + 0010000000000
- 整体转long 无符号 9657120577919624088 有符号 -8789623495789927528、
- 为啥有符号是负的 ,这是因为 28位时间差 最多表示 268,435,455秒 但 281,059,200 时间差明显超过了 ,再转二进制 就是1开头的29位二进制数 10000110000001001111110000000 这样 开头的标志位 0 就被时间差的第29位占用了,标志位是1 转 long 就是负数
- 通过调整时间位,让时间位够用 ,不占用标志位 就能 使long值为正数
- 调整后 30位二进制时间差 + 20位二进制机器号 + 13位 二进制序列号
- 二进制(时间差 281059200) + 二进制(29)+ 二进制(1024) 不够需要补全对应位置的位
- 010000110000001001111110000000 + 00000000000000011101 + 0010000000000
- 整体转long 值为 2414280144480085222 比目前id 小可能会有一系列问题
- 那 如何保证生成的id 大呢解析目前id 时间戳位置 前6位都是1 保证比之前id大 需要二进制时间戳前7位都是1 此时生成的id 比 之前的大 暂时称 1 的这几位数是 mask 位 减少 机器号位置增加mark位
- 此时 id = 7位二进制 (mask )+ 30位二进制时间差 + 13位二进制机器号 + 13位 二进制序列号
- 1111111 + 010000110000001001111110000000 + 0000000011101 + 0010000000000
- 整体转long 值为 9170176006445836224
- 达到不重复,不为负数,比之前id大的目的
代码调整后增加mask 位(标红位置为修改源码)
1 | /* |
备注:
- 修改之后如果时间位不够用生成 的id 可能重复,修改之前位数不够用先是负数后可能重复
- 修改之后如果代码涉及到id 解析的 需要修改对应解析方法