隐匿之符:一次关于负号的雪花追溯


在 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
2
3
protected int timeBits = 28;
protected int workerBits = 22;
protected int seqBits = 13;

配置解析

timeBits (28) workerBits (22) seqBits (13)
时间戳(秒) 机器号(固定为29) 每秒序列号

UID = (deltaSeconds << (22 + 13)) | (workerId << 13) | sequence;

  1. 序列号(sequence)

    2^13 - 1 = 8191

    • 最多能表示:8192 个唯一值
    • 范围是:0 ~ 8191(共 8192 个)
    • 所以在十进制里:最后最多就是 8191
  2. 机器号(workerId)

    2^22 - 1 = 4,194,304

    • 最多能表示:4,194,303个唯一值
    • 范围是:0 ~ 4,194,303(共 4,194,304个)
    • 所以在十进制里:最后最多就是 4,194,304
  3. 时间(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年之后会超变成负数

处理方案

目标 :

  1. 保证id 不重复

  2. 尽量保证id 递增趋势

    当前id :9090949948782127403L

    0111111000101001100010101101000000000000000000111010100100101011(64位)

    0 标志位

    1111110001010011000101011010 → 264581466 (时间差秒)

    0000000000000000011101 → 29 (机器号)

    0100100101011 → 2347 (序列号)

处理方法 : 调整uid 各组成位置位数

  1. 首先保证id 不重复可以通过修改机器号,不使用之前的机器id 生成uid 可以保证id 不与之前重复,即使时间戳一样

  2. 将时间差位置扩充以便能使用的时间更长,减少机器号位置

    30位 ≈ 31, 557,600 秒 ≈ 34.0年 机器号:20位=1,048,576个

    1
    2
    3
    protected int timeBits = 30;
    protected int workerBits = 20;
    protected int seqBits = 13;

    UID = (deltaSeconds << (20 + 13)) | (workerId << 13) | sequence;

  3. 只将时间戳位置 扩充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
    3
    protected 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/*
* Copyright (c) 2017 Baidu, Inc. All Rights Reserve.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* <http://www.apache.org/licenses/LICENSE-2.0>
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.baidu.fsg.uid;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.springframework.util.Assert;

/**
* Allocate 64 bits for the UID(long)<br>
* sign (fixed 1bit) -> deltaSecond -> workerId -> sequence(within the same second)
*
* @author yutianbao
*/
public class BitsAllocator {
/**
* Total 64 bits
*/
public static final int TOTAL_BITS = 1 << 6;

/**
* Bits for [sign-> mark -> second-> workId-> sequence]
*/
private int signBits = 1;
private final int timestampBits;
private final int workerIdBits;
private final int sequenceBits;

/**
* Max value for workId & sequence
*/
private final long maxDeltaSeconds;
private final long maxWorkerId;
private final long maxSequence;

/**
* Shift for timestamp & workerId
*/
private final int timestampShift;
private final int workerIdShift;

//面具位 7位为1 保证大于之前id 但需要每次算时间差 + 上前7为是1后30位为0的37位2进制数(1111111000000000000000000000000000000 =136365211648
// mask = (1L << maskLen) -1 面具值 = 1111111
//mask << maskShift = 1111111000000000000000000000000000000
private final int maskLen = 7;
private final long mask ;
private final int maskShift;

/**
* Constructor with timestampBits, workerIdBits, sequenceBits<br>
* The highest bit used for sign, so <code>63</code> bits for timestampBits, workerIdBits, sequenceBits
*/
public BitsAllocator(int timestampBits, int workerIdBits, int sequenceBits) {
// make sure allocated 64 bits
int allocateTotalBits = signBits + timestampBits + workerIdBits + sequenceBits;
Assert.isTrue(allocateTotalBits == TOTAL_BITS, "allocate not enough 64 bits");

// initialize bits
this.timestampBits = timestampBits;
this.workerIdBits = workerIdBits;
this.sequenceBits = sequenceBits;

// initialize max value
this.maxDeltaSeconds = ~(-1L << timestampBits);
this.maxWorkerId = ~(-1L << workerIdBits);
this.maxSequence = ~(-1L << sequenceBits);

// initialize shift
this.timestampShift = workerIdBits + sequenceBits;
this.workerIdShift = sequenceBits;
//面具参数相关值
this.mask = (1L << maskLen) -1 ;
this.maskShift = timestampShift + timestampBits - maskLen;
}

/**
* Allocate bits for UID according to delta seconds & workerId & sequence<br>
* <b>Note that: </b>The highest bit will always be 0 for sign
*
* @param deltaSeconds
* @param workerId
* @param sequence
* @return
*/
public long allocate(long deltaSeconds, long workerId, long sequence) {
return (mask << maskShift) | (deltaSeconds << timestampShift) | (workerId << workerIdShift) | sequence;
}

/**
* Getters
*/
public int getSignBits() {
return signBits;
}

public int getTimestampBits() {
return timestampBits;
}

public int getWorkerIdBits() {
return workerIdBits;
}

public int getSequenceBits() {
return sequenceBits;
}

public long getMaxDeltaSeconds() {
return maxDeltaSeconds;
}

public long getMaxWorkerId() {
return maxWorkerId;
}

public long getMaxSequence() {
return maxSequence;
}

public int getTimestampShift() {
return timestampShift;
}

public int getWorkerIdShift() {
return workerIdShift;
}

@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}

}

备注:

  1. 修改之后如果时间位不够用生成 的id 可能重复,修改之前位数不够用先是负数后可能重复
  2. 修改之后如果代码涉及到id 解析的 需要修改对应解析方法

文章作者: victor.smile
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 victor.smile !
  目录