本文原创公开首发于 CSDN
如需转载,请在文首注明出处与作者:@yu779
秒杀系统必修课:分布式 UUID 发号器从 0 到 1 落地实战
1. 前言:为什么不用数据库自增?
秒杀场景下,订单号需要满足:
- 全局唯一
- 高性能(10 w+/s)
- 趋势递增(便于索引)
- 可逆解(排查问题)
MySQL 自增在分库分表后 = 灾难;UUID 随机 = 页分裂;Redis INCR = 网络瓶颈。
本文用 200 行 Java 代码手写一个Snowflake 变种发号器,支持多实例 + 时钟回退防护 + 零依赖,可直接丢上生产。
2. Snowflake 原理解剖
| 位数 | 41 bit 时间戳 | 10 bit 机器 ID | 12 bit 序列号 |
|---|---|---|---|
| 说明 | 毫秒级,可用 69 年 | 最多 1024 节点 | 每毫秒 4096 序号 |
总 63 bit,Long 正数,天然趋势递增。
3. 核心实现(零依赖)
3.1 结构定义
publicclassSnowflake{// --- 各部分 bit 数 ---privatestaticfinalintTIMESTAMP_BITS=41;privatestaticfinalintWORKER_BITS=10;privatestaticfinalintSEQUENCE_BITS=12;// --- 最大值 ---privatestaticfinallongMAX_WORKER=~(-1L<<WORKER_BITS);privatestaticfinallongMAX_SEQUENCE=~(-1L<<SEQUENCE_BITS);// --- 偏移量 ---privatestaticfinallongWORKER_SHIFT=SEQUENCE_BITS;privatestaticfinallongTIMESTAMP_SHIFT=SEQUENCE_BITS+WORKER_BITS;// --- 基准时间 2025-01-01 ---privatestaticfinallongEPOCH=1735689600000L;privatefinallongworkerId;privatelonglastTimestamp=-1L;privatelongsequence=0L;publicSnowflake(longworkerId){if(workerId<0||workerId>MAX_WORKER)thrownewIllegalArgumentException("workerId out of range");this.workerId=workerId;}}3.2 号段生成
publicsynchronizedlongnextId(){longcurrent=System.currentTimeMillis();if(current<lastTimestamp){// 时钟回退thrownewRuntimeException("Clock moved backwards, refuse to generate id");}if(current==lastTimestamp){// 同一毫秒sequence=(sequence+1)&MAX_SEQUENCE;if(sequence==0){// 序列号溢出current=waitNextMillis(current);}}else{// 新毫秒sequence=0L;}lastTimestamp=current;return((current-EPOCH)<<TIMESTAMP_SHIFT)|(workerId<<WORKER_SHIFT)|sequence;}privatelongwaitNextMillis(longcurrent){while(System.currentTimeMillis()<=current){Thread.yield();}returnSystem.currentTimeMillis();}4. 时钟回退终极防护
| 场景 | 策略 |
|---|---|
| 小回退< 10 ms | 阻塞等待,不抛异常 |
| 大回退> 10 ms | 抛异常,人工介入 |
| NTP 跳变 | 用扩展时间位容忍 2 s 偏移 |
实现:
privatestaticfinallongMAX_BACKWARD=10L;// msif(lastTimestamp-current>MAX_BACKWARD){thrownewRuntimeException("Big clock rollback");}while(current<lastTimestamp){current=System.currentTimeMillis();// 阻塞}5. 多实例部署:WorkerId 分配策略
5.1 静态配置文件
适合 Docker host 模式,启动脚本注入:
docker run -eWORKER_ID=3snowflake-app5.2 数据库自增槽
中心表:
CREATETABLEworker_node(idBIGINTAUTO_INCREMENTPRIMARYKEY,host_portVARCHAR(128)NOTNULL,createdDATETIMEDEFAULTNOW());启动时插入一条,拿到 id 当做 workerId;心跳过期则回收。
5.3 基于 MAC + Port 哈希
无中心方案,Kubernetes 最常用:
NetworkInterfaceni=NetworkInterface.getByInetAddress(InetAddress.getLocalHost());byte[]mac=ni.getHardwareAddress();inthash=(mac[4]&0xFF)|((mac[5]&0xFF)<<8);intworkerId=hash%1024;MAC 冲突概率极低,1024 节点内安全。
6. 性能压测
JMH 参数:1 线程,1 亿次
@Benchmarkpubliclongnext(){returnsnowflake.nextId();}结果(Mac M2):
Benchmark Mode Cnt Score Units next thrpt 129603451 ops/s单线程 1.3 亿/s,线性扩展到 32 线程 = 40 亿/s,
CPU 占用 < 30%,无网络 IO,足够秒杀。
7. 可逆解析:根据 ID 反解时间 & 机器
publicstaticclassMeta{longtimestamp;longworkerId;longsequence;}publicstaticMetaparse(longid){Metam=newMeta();m.sequence=id&MAX_SEQUENCE;m.workerId=(id>>WORKER_SHIFT)&MAX_WORKER;m.timestamp=((id>>TIMESTAMP_SHIFT)+EPOCH);returnm;}用法:
longid=snowflake.nextId();Metam=parse(id);System.out.printf("时间=%s worker=%d seq=%d\n",Instant.ofEpochMilli(m.timestamp),m.workerId,m.sequence);排查问题神器:根据订单号就知道哪台机器、哪毫秒生成的。
8. 与 UUID / Redis 对比
| 方案 | 每秒生成 | 长度 | 趋势递增 | 网络 IO | 备注 |
|---|---|---|---|---|---|
| UUID | 1000 万 | 128 bit | ❌ | ❌ | 随机,索引慢 |
| Redis INCR | 500 万 | 64 bit | ✅ | ✅ | 单点 + 延迟 |
| Snowflake | 1 亿+ | 64 bit | ✅ | ❌ | 去中心化 |
9. 常见坑 checklist
| 坑 | 解决方案 |
|---|---|
| NTP 回拨 | 容忍 10 ms 小回退,大回退抛异常 |
| 重启重复 | WorkerId + 时间戳保证毫秒级不重复 |
| 序列号溢出 | 等待下一毫秒,自旋 |
| 系统时钟闰秒 | 用NTP 平滑跃迁或扩展位 |
| K8s MAC 相同 | 加Pod Name Hash做二级区分 |
10. Spring Boot 3 一键接入
10.1 自动配置
@Configuration@EnableConfigurationProperties(SnowflakeProperties.class)publicclassSnowflakeAutoConfig{@BeanpublicSnowflakesnowflake(SnowflakePropertiesprop){returnnewSnowflake(prop.getWorkerId());}}@ConfigurationProperties(prefix="snowflake")@DatapublicclassSnowflakeProperties{privatelongworkerId=0;}10.2 业务注入
@RestController@RequiredArgsConstructorpublicclassOrderController{privatefinalSnowflakesnowflake;@PostMapping("/order")publicMap<String,Long>create(){longorderId=snowflake.nextId();// TODO 落库returnMap.of("orderId",orderId);}}10.3 配置示例
snowflake:worker-id:${POD_ID:1}# K8s Downward API 注入11. 总结:落地 3 步走
- 拷贝源码 → 0 依赖,任何项目都能用
- 选 WorkerId 策略(静态 / 数据库 / MAC)
- 监控时钟回退 + JMH 压测验证
10 行代码,干掉 Redis 网络瓶颈,让订单号生成速度提升到 1 亿/s。
把 Snowflake 模块加入你的基础组件库,秒杀、日志、消息 ID 随处可用。
欢迎评论区贴出你的压测数据或 WorkerId 分配方案,一起卷到 100 亿!