雪花算法升级实践:从 Snowflake 到 SnowflakeV2,确保 ID 增长不溢出
一、问题背景:默认起始时间导致的潜在风险
雪花算法(Snowflake)是一种常见的分布式 ID 生成方案。它通过将 64 位的 long 型整数拆分为不同字段(时间戳、机器 ID、序列号),在高并发分布式环境中生成全局唯一且有序的 ID。
由于 使用NewLife未设置开始时间,默认使用1970作为纪元时间,并已经产生很多数据 ,导致无法直接在改动NewLife的纪元时间,到达延长雪花算法的使用。
NewLife 框架提供的 Snowflake 实现默认使用 1970-01-01 作为起始时间,这意味着:
- 时间戳占 41 位,可支持约 69 年(2^41 毫秒 ≈ 69 年);
- 因此,默认配置的有效期约为 1970 ~ 2040 年;
- 超过 2040 年后,时间戳溢出,ID 将出现重复或倒退风险。
由于当前时间已经来到 2025 年,为避免系统在未来 15 年内因雪花算法时间耗尽而出现数据主键冲突问题,必须尽快进行升级处理。
更关键的是: 该 ID 被用作 数据库主键,因此新算法必须确保:
- 新生成的 ID 比旧 ID 大(以便迁移时保持递增性);
- ID 仍然唯一且有序;
- 与原有结构兼容,便于平滑替换。
二、原 Snowflake 算法结构回顾
原始 Snowflake 结构如下:
| 位段 | 位数 | 含义 |
|---|---|---|
| 符号位 | 1 | 固定为 0 |
| 时间戳 | 41 | 从 1970 年开始的毫秒数 |
| 机器 ID | 10 | 支持最多 1024 节点 |
| 序列号 | 12 | 每毫秒最多 4096 个 ID |
示意:
1
0 | 41-bit 时间戳 | 10-bit 机器 ID | 12-bit 序列号
由于时间戳起点固定在 1970 年,系统时间一旦超过 2040 年,将无法再保证新 ID 比旧 ID 大。
三、升级目标与设计思路
✅ 目标
- 时间起点迁移到 2025 年;
- 新算法生成的 ID 必须 大于旧算法生成的最大 ID(7392450313976012800);
- 保留原有的 64 位结构,兼容数据库字段类型;
- 保证分布式场景下的全局唯一性与递增性。
💡 设计思路
为确保新 ID 永远大于旧 ID,可通过 在最高位增加一个前缀标识位(PrefixValue) 来实现。
SnowflakeV2 的核心结构定义如下:
| 位段 | 位数 | 含义 |
|---|---|---|
| 固定前缀 | 4 | 二进制 1101(十进制 13),确保新 ID 大于旧 ID |
| 时间戳 | 41 | 从 2025-01-01 起的毫秒数 |
| 机器 ID | 6 | 支持 64 节点 |
| 序列号 | 12 | 每毫秒支持 4096 个 ID |
对应二进制结构:
1
1101 | 41-bit 时间戳 | 6-bit 机器 ID | 12-bit 序列号
四、关键实现与位运算逻辑
核心代码片段如下:
1
2
3
4
5
6
7
private static Int64 BuildSnowflakeId(Int64 timestamp, Int32 workerId, Int32 sequence)
{
return ((Int64)PrefixValue << PrefixShift) |
((timestamp & MaxTimestamp) << TimestampShift) |
((Int64)(workerId & MaxWorkerId) << WorkerIdShift) |
(Int64)(sequence & MaxSequence);
}
1️⃣ 各部分解释:
PrefixValue:固定为0b1101(二进制 1101,即十进制 13);PrefixShift:= 41(时间戳位) + 6(机器位) + 12(序列位) = 59;TimestampShift:= 6 + 12 = 18;WorkerIdShift:= 12;& MaxXXX:用于防止位数溢出;|:按位或,用于拼接二进制段。
2️⃣ 为什么选 13 作为前缀?
经过计算:
1
2
8 << 59 = 4.61168601842739e18 (十进制开头为4)
13 << 59 = 7.49498978994993e18 (大于 7392450313976012800)
因此,13(二进制 1101)是满足“大于旧 ID”的最小前缀值。
五、SnowflakeV2 源码实现
下面是完整源码,基于 NewLife 框架的 Snowflake 改造而来:
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
using NewLife.Caching;
using NewLife.Log;
using NewLife.Model;
using NewLife.Security;
using System;
using System.Diagnostics;
using System.Threading;
namespace NewLife.Data;
/// <summary>雪花算法V2。分布式Id生成器,确保ID比原Id大</summary>
/// <remarks>
/// Snowflake 已经禁止使用(Snowflake使用时未设置初始时间,默认是1970年,最多支持到2040年,故提前升级处理,避免数字耗尽(数据在递增)后无法处理),
/// 使用SnowflakeV2只能通过注入形式获取(初始化时已经单例注入)。不能直接new(直接new会丢失开始时间配置,会默认事件开始)。
/// SnowflakeV2已经处理最新Id 大于当前时间(2025-12-01)Snowflake.NewId生成的Id。
/// 改进版雪花算法,确保ID比原Id大
///
/// 使用一个 64 bit 的 long 型的数字作为全局唯一 id(符号位为0,实际63位有效)
/// 结构:4bit标识(固定1101) + 41bit时间戳 + 6bit机器 + 12bit序列号
///
/// 相比标准雪花算法的改进:
/// - 4位固定标识:1101(二进制),确保ID比原Id大
/// - 41位时间戳:约69年有效期(与标准雪花算法相同)
/// - 6位机器ID:支持0-63个节点(原10位支持1024)
/// - 12位序列号:每毫秒4096个ID(与标准雪花算法相同)
///
/// 务必请保证SnowflakeV2对象的唯一性,特别在使用XCode等数据中间件时,要确保每张表只有一个Snowflake实例。
/// </remarks>
public class SnowflakeV2
{
#region 静态常量
/// <summary>标识位数(固定为1000,即8)</summary>
private const Int32 PrefixBits = 4;
/// <summary>固定前缀值(二进制1101)[这里最终得到的值要比原最大Id(7392450313976012800)大]</summary>
private const Int32 PrefixValue = 0b1101;
/// <summary>时间戳位数(保持41位)</summary>
private const Int32 TimestampBits = 41;
/// <summary>工作节点ID位数(压缩到6位)</summary>
private const Int32 WorkerIdBits = 6;
/// <summary>序列号位数(保持12位)</summary>
private const Int32 SequenceBits = 12;
/// <summary>最大工作节点ID(63)</summary>
private const Int32 MaxWorkerId = (1 << WorkerIdBits) - 1;
/// <summary>最大序列号(4095)</summary>
private const Int32 MaxSequence = (1 << SequenceBits) - 1;
/// <summary>时间戳左移位数</summary>
private const Int32 TimestampShift = WorkerIdBits + SequenceBits;
/// <summary>前缀左移位数</summary>
private const Int32 PrefixShift = TimestampBits + WorkerIdBits + SequenceBits;
/// <summary>工作节点ID左移位数</summary>
private const Int32 WorkerIdShift = SequenceBits;
/// <summary>时间回拨最大容忍度(毫秒)</summary>
private const Int64 MaxClockBack = 3600_000 + 10_000;
/// <summary>时间戳最大值(41位)</summary>
private const Int64 MaxTimestamp = (1L << TimestampBits) - 1;
#endregion
#region 属性
/// <summary>开始时间戳。首次使用前设置,否则无效,默认1970-1-1</summary>
/// <remarks>
/// 41位时间戳约69年有效期,与标准雪花算法相同
/// </remarks>
public DateTime StartTimestamp { get; set; } = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
private Int32 _workerId;
/// <summary>机器Id,取6位(0-63)</summary>
public Int32 WorkerId
{
get => _workerId;
set
{
if (value is < 0 or > MaxWorkerId)
throw new ArgumentOutOfRangeException(nameof(value), $"WorkerId必须在0-{MaxWorkerId}范围内");
_workerId = value;
}
}
private Int32 _sequence;
/// <summary>序列号,取12位(0-4095)。进程内静态,避免多个实例生成重复Id</summary>
public Int32 Sequence => _sequence;
/// <summary>全局机器Id。若设置,所有雪花实例都将使用该Id,可以由星尘配置中心提供本应用全局唯一机器码,且跨多环境唯一</summary>
public static Int32 GlobalWorkerId { get; set; }
/// <summary>workerId分配集群。配置后可确保所有实例化的雪花对象得到唯一workerId,建议使用Redis</summary>
public static ICache? Cluster { get; set; }
private Int64 _lastTimestamp;
#endregion
#region 构造函数
private static Int32 _globalInstanceId;
private static readonly Int32 _defaultInstanceId;
static SnowflakeV2()
{
try
{
var provider = ObjectContainer.Provider?.GetService<ICacheProvider>();
if (provider is { Cache: not MemoryCache } && provider.Cache != provider.InnerCache)
Cluster = provider.Cache;
var ip = NetHelper.MyIP();
if (ip != null)
{
var buf = ip.GetAddressBytes();
_defaultInstanceId = buf[3] & 0x3F; // 取最后6位(0-63)
}
else
{
_defaultInstanceId = Rand.Next(1, 64);
}
}
catch
{
_defaultInstanceId = Rand.Next(1, 64);
}
}
#endregion
#region 核心方法
private Boolean _initialized;
private readonly Object _lockObject = new();
/// <summary>初始化WorkerId</summary>
private void Initialize()
{
if (_initialized) return;
lock (_lockObject)
{
if (_initialized) return;
using var span = DefaultTracer.Instance?.NewSpan("SnowflakeV2-Init", new { id = Interlocked.Increment(ref _globalInstanceId) });
if (WorkerId <= 0 && GlobalWorkerId > 0)
WorkerId = GlobalWorkerId & MaxWorkerId;
if (WorkerId <= 0 && Cluster != null)
JoinCluster(Cluster);
if (WorkerId <= 0)
{
var nodeId = _defaultInstanceId;
var pid = Process.GetCurrentProcess().Id;
var tid = Thread.CurrentThread.ManagedThreadId;
// 6位WorkerId,混合IP、进程、线程
WorkerId = (nodeId ^ (pid & 0x3F) ^ (tid & 0x3F)) & MaxWorkerId;
}
span?.AppendTag($"WorkerId={WorkerId} StartTimestamp={StartTimestamp.ToFullString()}");
_initialized = true;
}
}
/// <summary>获取下一个Id</summary>
/// <remarks>基于当前时间,转StartTimestamp所属时区后,生成Id</remarks>
/// <returns>雪花Id(8开头)</returns>
public virtual Int64 NewId()
{
Initialize();
var currentTimestamp = (Int64)(ConvertKind(DateTime.Now) - StartTimestamp).TotalMilliseconds;
var workerId = WorkerId & MaxWorkerId;
var lastTime = Volatile.Read(ref _lastTimestamp);
var timestamp = currentTimestamp;
if (currentTimestamp < lastTime)
{
var clockBack = lastTime - currentTimestamp;
if (clockBack > MaxClockBack)
throw new InvalidOperationException($"时间回拨过大 ({clockBack}ms)。为保证唯一性,雪花算法拒绝生成新Id");
timestamp = lastTime;
}
var sequence = 0;
lock (_lockObject)
{
while (true)
{
if (timestamp > _lastTimestamp)
{
_sequence = 0;
_lastTimestamp = timestamp;
sequence = 0;
break;
}
if (timestamp == _lastTimestamp)
{
sequence = Interlocked.Increment(ref _sequence);
if (sequence <= MaxSequence) break;
timestamp = _lastTimestamp + 1;
_sequence = 0;
}
else
{
timestamp = _lastTimestamp;
}
}
}
return BuildSnowflakeId(timestamp, workerId, sequence);
}
/// <summary>获取指定时间的Id,带上节点和序列号。可用于根据业务时间构造插入Id</summary>
/// <remarks>
/// 基于指定时间,转StartTimestamp所属时区后,生成Id。
/// 如果为指定毫秒时间生成多个Id(超过4096),则可能重复。
/// </remarks>
/// <param name="time">时间</param>
/// <returns>雪花Id(8开头)</returns>
public virtual Int64 NewId(DateTime time)
{
Initialize();
time = ConvertKind(time);
var timestamp = (Int64)(time - StartTimestamp).TotalMilliseconds;
var workerId = WorkerId & MaxWorkerId;
var sequence = Interlocked.Increment(ref _sequence) & MaxSequence;
return BuildSnowflakeId(timestamp, workerId, sequence);
}
/// <summary>获取指定时间的Id,传入唯一业务id(取模为6位)。可用于物联网数据采集,每64个传感器为一组,每组每毫秒多个Id</summary>
/// <remarks>
/// 基于指定时间,转StartTimestamp所属时区后,生成Id。
///
/// 在物联网数据采集中,数据分析需要,更多希望能够按照采集时间去存储。
/// 为了避免主键重复,可以使用传感器id作为workerId。
/// uid需要取模为6位,即按64分组,每组每毫秒最多生成4096个Id。
///
/// 如果为指定分组在特定毫秒时间生成多个Id(超过4096),则可能重复。
/// </remarks>
/// <param name="time">时间</param>
/// <param name="uid">唯一业务id。例如传感器id</param>
/// <returns>雪花Id(8开头)</returns>
public virtual Int64 NewId(DateTime time, Int32 uid)
{
Initialize();
time = ConvertKind(time);
var timestamp = (Int64)(time - StartTimestamp).TotalMilliseconds;
var workerId = uid & MaxWorkerId;
var sequence = Interlocked.Increment(ref _sequence) & MaxSequence;
return BuildSnowflakeId(timestamp, workerId, sequence);
}
/// <summary>获取指定时间的Id,传入唯一业务id(18位)。可用于物联网数据采集,每262144个传感器一组,每组每毫秒1个Id</summary>
/// <remarks>
/// 基于指定时间,转StartTimestamp所属时区后,生成Id。
///
/// 在物联网数据采集中,数据分析需要,更多希望能够按照采集时间去存储。
/// 为了避免主键重复,可以使用传感器id作为workerId。
/// 再配合upsert写入数据,如果同一个毫秒内传感器有多行数据,则只会插入一行。
///
/// 如果为指定业务id在特定毫秒时间生成多个Id(超过1个),则可能重复。
/// </remarks>
/// <param name="time">时间</param>
/// <param name="uid">唯一业务id。例如传感器id</param>
/// <returns>雪花Id(8开头)</returns>
public virtual Int64 NewId22(DateTime time, Int32 uid)
{
Initialize();
time = ConvertKind(time);
var timestamp = (Int64)(time - StartTimestamp).TotalMilliseconds;
var workerId = uid & ((1 << 18) - 1); // 18位业务ID(6+12位)
return ((Int64)PrefixValue << PrefixShift) |
((timestamp & MaxTimestamp) << TimestampShift) |
(Int64)workerId;
}
/// <summary>时间转为Id,不带节点和序列号。可用于构建时间片段查询</summary>
/// <remarks>
/// 基于指定时间,转StartTimestamp所属时区后,生成不带WorkerId和序列号的Id。
/// 一般用于构建时间片段查询,例如查询某个时间段内的数据,把时间片段转为雪花Id片段。
/// </remarks>
/// <param name="time">时间</param>
/// <returns>时间部分的Id(8开头)</returns>
public virtual Int64 GetId(DateTime time)
{
time = ConvertKind(time);
var timestamp = (Int64)(time - StartTimestamp).TotalMilliseconds;
return ((Int64)PrefixValue << PrefixShift) |
((timestamp & MaxTimestamp) << TimestampShift);
}
/// <summary>解析雪花Id,得到时间、WorkerId和序列号</summary>
/// <remarks>
/// 其中的时间是StartTimestamp所属时区的时间。
/// </remarks>
/// <param name="id">雪花Id</param>
/// <param name="time">解析出的时间</param>
/// <param name="workerId">解析出的工作节点Id</param>
/// <param name="sequence">解析出的序列号</param>
/// <returns>是否解析成功</returns>
public virtual Boolean TryParse(Int64 id, out DateTime time, out Int32 workerId, out Int32 sequence)
{
// 验证前缀(可选,根据需要决定是否严格校验)
var prefix = (Int32)(id >> PrefixShift) & 0xF;
if (prefix != PrefixValue)
{
// 如果前缀不是8,可能是老版本ID,也尝试解析
// 或者直接返回false,根据业务需求决定
}
var timestamp = (id >> TimestampShift) & MaxTimestamp;
time = StartTimestamp.AddMilliseconds(timestamp);
workerId = (Int32)((id >> WorkerIdShift) & MaxWorkerId);
sequence = (Int32)(id & MaxSequence);
return true;
}
/// <summary>把输入时间转为开始时间戳的类型,便于相减</summary>
/// <param name="time">要转换的时间</param>
/// <returns>转换后的时间</returns>
public DateTime ConvertKind(DateTime time)
{
if (time.Kind == DateTimeKind.Unspecified) return time;
return StartTimestamp.Kind switch
{
DateTimeKind.Utc => time.ToUniversalTime(),
DateTimeKind.Local => time.ToLocalTime(),
_ => time,
};
}
#endregion
#region 私有辅助方法
/// <summary>构建雪花Id(8开头)</summary>
private static Int64 BuildSnowflakeId(Int64 timestamp, Int32 workerId, Int32 sequence)
{
return ((Int64)PrefixValue << PrefixShift) |
((timestamp & MaxTimestamp) << TimestampShift) |
((Int64)(workerId & MaxWorkerId) << WorkerIdShift) |
(Int64)(sequence & MaxSequence);
}
#endregion
#region 集群扩展
/// <summary>加入集群。由集群统一分配WorkerId,确保唯一,从而保证生成的雪花Id绝对唯一</summary>
/// <param name="cache">缓存实例,通常是Redis</param>
/// <param name="key">分配WorkerId的缓存键</param>
public virtual void JoinCluster(ICache cache, String key = "SnowflakeV2WorkerId")
{
if (cache == null) throw new ArgumentNullException(nameof(cache));
var workerId = (Int32)cache.Increment(key, 1);
WorkerId = workerId & MaxWorkerId;
}
#endregion
}
六、兼容性验证
通过单元测试验证新旧版本 ID 的递增关系:
1
2
3
4
5
6
7
8
9
10
11
12
[Fact(DisplayName = "V2版本的Id大于V1版本")]
public void V2EqV1()
{
var snowflake = new Snowflake();
var id = snowflake.NewId();
var snowflake2 = new SnowflakeV2();
snowflake2.StartTimestamp = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var id2 = snowflake2.NewId();
Assert.True(id2 > id);
}
另一个测试验证迁移时间:
1
2
3
4
5
6
7
8
9
10
11
12
[Fact(DisplayName = "V2版本的Id大于V1版本,迁移时间2025年12月之前")]
public void V2EqV1WithMigrateDate()
{
var snowflake = new Snowflake();
var id = snowflake.NewId(new DateTime(2025, 12, 1, 0, 0, 0, DateTimeKind.Utc));
var snowflake2 = new SnowflakeV2();
snowflake2.StartTimestamp = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var id2 = snowflake2.NewId();
Assert.True(id2 > id);
}
✅ 测试结果: 在 2025 年迁移后,SnowflakeV2 生成的 ID 始终大于旧版 Snowflake 的最大 ID,且仍保持递增与唯一性。
七、迁移与使用建议
- 迁移时机:建议在 2025 年前完成切换;
- 替换方式:通过依赖注入替换
Snowflake为SnowflakeV2; 初始化配置:
1 2
var snowflake2 = ObjectContainer.Resolve<SnowflakeV2>(); snowflake2.StartTimestamp = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
注意事项:
SnowflakeV2不可直接new,应由容器单例注入;- 同一张表中必须保证唯一的
SnowflakeV2实例; - 分布式场景建议通过 Redis 分配
WorkerId。
八、结论
SnowflakeV2 是基于 NewLife 框架原雪花算法的升级版本,核心改进如下:
| 特性 | Snowflake (旧) | SnowflakeV2 (新) |
|---|---|---|
| 时间起点 | 1970-01-01 | 2025-01-01 |
| 前缀标识 | 无 | 固定 1101(二进制) |
| 机器位 | 10 位 | 6 位 |
| ID 范围 | 1970~2040 | 2025~2094 |
| ID 递增性 | 有序 | 保持递增且大于旧 ID |
该方案不仅保证了分布式全局唯一性和顺序性,还解决了时间溢出问题,是对原雪花算法的工程化优化。
✨ 总结
通过在最高位增加前缀标识(1101),并重新定义时间起点为 2025 年,SnowflakeV2 兼容原有结构、保持 ID 递增,并避免了 2040 年的时间耗尽风险。这是一次兼顾向前兼容与未来可扩展性的架构升级。