嘿,朋友!想象一下,你是一家拥有几千台服务器的互联网大厂架构师。现在有个小麻烦:你的订单系统每秒钟要生成几万个订单号。
如果让数据库自己搞(自增ID),几台数据库凑在一起,肯定会出现"撞车"------大家都生成了ID 1001,这就像两个新生儿都叫"张三",户籍科(数据库)当场就得炸毛。
这时候,Twitter 的那帮天才掏出了一个叫 雪花算法 的宝贝。这玩意儿生成的 ID 不长,还是个数字,但就像雪花一样,世界上没有两片完全相同的雪花。
它是怎么做到的呢?全靠"拼积木"。
核心原理:把时间、地点、序号拼成"身份证"
雪花算法生成的 ID 是一个 64 位的长整型数字 (在 Java 里是 long 类型)。这 64 位并不是随机乱填的,而是像切蛋糕一样,被切成了几块,每一块都有特殊的含义。
我们可以把它看作一张**"数字身份证"**:
| 积木块 | 位数 | 含义 | 通俗解释 |
|---|---|---|---|
| 符号位 | 1 位 | 固定为 0 | 我是好人:保证生成的 ID 是正数,别搞成负数吓唬人。 |
| 时间戳 | 41 位 | 毫秒级时间 | 出生时间:记录你出生的精确时刻(精确到毫秒)。 |
| 机器ID | 10 位 | 数据中心+机器 | 籍贯/住址:你是北京机房 3 号服务器生的,还是上海机房 1 号生的。 |
| 序列号 | 12 位 | 自增数字 | 同毫秒排行:如果同一毫秒生了好几个,那就排号:001, 002, 003... |
为什么这样拼就不会重复?
这就好比给新生儿起名:
- 时间不同:只要你不是穿越回来的,时间戳肯定不一样,ID 就不一样。
- 机器不同:就算时间一样(同一毫秒),你是 1 号机器生的,我是 2 号机器生的,机器 ID 不同,ID 也不一样。
- 序号不同:就算时间一样、机器也一样(高并发),那咱们就排号,你是老大,我是老二,序号不同,ID 还是不一样。
结论 :只要机器 ID 不冲突,这 ID 就绝对全局唯一!
它的"超能力":为什么大家都爱它?
趋势递增(数据库的最爱)
因为 ID 的高位是时间戳,时间是往前走的,所以 ID 总体上也是越来越大的。
- 好处:数据库(尤其是 MySQL 的 InnoDB 引擎)最喜欢自增的主键。如果你用 UUID(乱码一样的字符串),数据库索引会分裂,性能掉渣。用雪花算法,数据库写入速度嗖嗖的,就像排队买票一样井然有序。
高性能(闪电侠)
生成这个 ID 不需要去查数据库,也不需要去请求 Redis。它纯粹是内存计算,靠的是位运算(左移、或运算)。
- 速度 :单机每秒能生成百万级的 ID。这就好比你不需要去派出所办证,自己在家里拿个印章盖一下就算数了,快得离谱。
简单粗暴
不依赖任何第三方服务(Zookeeper、Redis 等),只要给每台机器分配个编号,代码里算一算就完事了。
它的"阿喀琉斯之踵":时钟回拨
虽然雪花算法很完美,但它有一个致命的弱点:它是个"时间控"。
它强依赖服务器的时间。如果有一天,你的服务器因为 NTP 时间同步或者其他原因,时间突然往回拨了(比如从 12:00:05 变成了 12:00:01),那就会出大事!
- 后果 :机器可能会用"过去的时间"生成一个已经生成过的 ID,导致ID 重复。
- 解决 :
- 暴力派:一旦发现时间回拨,直接抛出异常,拒绝服务("别干了,时间乱了,我罢工!")。
- 温和派:如果发现回拨时间很短(比如几毫秒),就稍微等一等,等时间追上来再生成。
逛一逛ID 生成博览会
| 方案 | 核心原理 | 优点 | 缺点(槽点) | 适合场景 |
|---|---|---|---|---|
| UUID | 随机字符串 | 本地生成,极快,不依赖外部 | 太长、无序,数据库索引噩梦 | 临时文件、日志 ID、非主键 |
| 数据库自增 | 单表 auto_increment |
简单,天然有序 | 单点故障,并发低,扩展难 | 只有单库的小项目 |
| Redis 自增 | INCR 命令 |
性能好,天然有序 | 依赖 Redis,持久化没做好会丢号 | 订单流水号、计数器 |
| 号段模式 | 批量取号,内存发号 | 数据库压力小,容灾强 | 依赖数据库,ID 连续(易被爬取) | 核心业务(美团 Leaf、滴滴 TinyID) |
| 百度 UidGenerator | 雪花改良+环形缓存 | 性能怪兽(QPS 600万+) | 依赖 Zookeeper,配置复杂 | 超高并发场景 |
详细解说:各路神仙显神通
UUID:随性的"流浪艺术家"
- 原理 :基于 MAC 地址、时间戳、随机数生成的一串 32 位的字符串(如
550e8400-e29b...)。 - 槽点 :
- 太长 :存一个 ID 占 36 个字符,比
bigint多花好几倍空间。 - 无序:插入数据库时,索引树会频繁分裂(就像把书随机插进书架,得不停挪位置),写入性能极差。
- 太长 :存一个 ID 占 36 个字符,比
- 评价 :除非你不需要存数据库,或者只是用来做临时文件名,否则别拿它当主键。
2. 数据库自增:老旧的"户籍科"
- 原理 :利用 MySQL 的
auto_increment。为了防冲突,可以设置步长(比如库 1 每次加 2,库 2 每次加 2 但起始是 2)。 - 槽点 :
- 单点瓶颈:所有 ID 请求都打向数据库,数据库挂了,全系统瘫痪。
- 扩展难:想加机器?得重新规划步长,还得停机维护。
- 评价:适合还在"作坊式"开发的小项目,大厂早就抛弃了。
3. Redis 自增:高效的"打号机"
- 原理 :利用 Redis 的单线程原子性,执行
INCR key命令。 - 优点:速度极快(内存操作),生成的 ID 还是连续递增的。
- 槽点 :
- 依赖 Redis:Redis 挂了,ID 生成也就停了。
- 数据丢失风险 :如果 Redis 重启时 RDB/AOF 没持久化好,ID 可能会重复(比如重启后又从 1 开始计数)。
- 评价 :适合做"流水号"(如
202310270001),配合日期前缀使用效果更佳。
4. 号段模式:聪明的"批发商"
这是目前大厂(美团、滴滴)非常流行的方案,也是雪花算法的强力竞争者。
- 原理 :
- 应用启动时,去数据库批量申请一段 ID(比如 1 到 1000),存在本地内存里。
- 发 ID 时,直接从内存里拿,拿完了再去数据库申请下一段(1001 到 2000)。
- 双 Buffer 机制:为了防止申请下一段时卡顿,通常会搞两个缓冲区(A 和 B)。A 快用完时,后台线程悄悄去把 B 填满。A 用完瞬间切换到 B,用户体验不到卡顿。
- 优点 :
- 数据库压力极小:原来每生成一个 ID 都要写库,现在每 1000 个才写一次。
- 容灾强:就算数据库挂了,本地内存里还有几百个 ID 能发,够撑一会儿让运维去修了。
- 代表选手 :
- 美团 Leaf:支持号段模式和雪花模式,功能强大,但依赖 Zookeeper 和 DB,部署略重。
- 滴滴 TinyID :专注于号段模式,支持双号段缓存,简单好用。
5. 百度 UidGenerator:性能怪兽
- 原理 :基于雪花算法改良,但为了解决雪花算法"生成 ID 还要算时间"的微小耗时,它搞了个 RingBuffer(环形缓冲区)。
- 绝招 :
- 它像个"预生成工厂",后台线程不停地生成 ID 塞进环形队列里。
- 业务线程要 ID 时,直接从队列里纯内存读取,连位运算都省了。
- QPS 能达到 600 万+,非常恐怖。
- 评价:适合对性能有极致要求的场景,但配置比较复杂,得维护 Zookeeper。
- 不想折腾,只想快点上线 :用 Redis 自增(记得配好持久化)。
- 追求极致性能,不怕配置麻烦 :用 百度 UidGenerator。
- 追求稳定,业务量大,且不想有单点故障 :用 美团 Leaf(号段模式)。这是目前最推荐的通用方案。
- 如果不怕时钟回拨的坑 :继续用 雪花算法(或者找个成熟的封装库,如 Twitter 的 Snowflake 改进版)。
一句话建议 :如果是核心业务(如订单、支付),号段模式(Leaf/TinyID) 是最稳妥的选择;如果是日志、消息追踪,雪花算法 或 UUID 就足够了。
号段模式:你值得单开一栏
之前也说了,不过想再仔细一些的:"空间换时间" 和**"批量换单次"** 的策略,将原本需要频繁访问数据库的操作,转化为了高效的本地内存操作
战术一:批量预取(Batch Pre-fetching)------ 减少数据库交互
这是号段模式的基石。在高并发下,如果每生成一个 ID 都去请求一次数据库,数据库的锁竞争和网络 IO 会成为巨大的瓶颈。
- 原理 :应用服务启动或号段耗尽时,向数据库申请一段 连续的 ID(例如步长
step=1000,申请1001~2000)。 - 效果 :原本需要 1000 次 数据库交互,现在只需要 1 次。这使得数据库的 QPS 压力降低了几个数量级,即使后端数据库性能一般,前端也能轻松抗住每秒数万甚至十万的请求。
战术二:本地内存分配(Local Memory Allocation)------ 极致性能
拿到号段后,ID 的生成过程完全在应用服务的本地内存中进行。
- 原理 :服务在内存 中维护一个当前号段 (如
1001~2000)和一个当前指针 。每次请求 ID,只需将指针+1并返回,无需任何网络 IO。 - 效果 :内存操作的速度是纳秒级的,这使得单机生成 ID 的 QPS 可以轻松突破 10万+,甚至更高,完全受限于 CPU 性能而非数据库性能。
战术三:双缓冲机制(Double Buffering)------ 消除等待
如果等到号段彻底用完(例如发到 2000)才去申请下一段,那么在申请新号段的几百毫秒内,业务线程会被阻塞。为了解决这个问题,号段模式引入了"双缓冲"或"异步预加载"。
- 原理 :
- 维护两个号段:内存中同时持有"当前号段"和"下一个号段"。
- 阈值触发 :当"当前号段"使用到一定比例(例如 80% ,即发到 1800)时,后台异步线程立即去数据库申请下一个号段(
2001~3000)并加载到备用缓冲区。 - 无缝切换 :当"当前号段"发完(2000)时,直接切换到已经准备好的"下一个号段",业务线程零等待。
- 效果:彻底消除了因数据库网络延迟导致的业务阻塞,保证了高并发下的平滑过渡。
战术四:数据库乐观锁(Optimistic Locking)------ 保证并发安全
在多个应用实例(集群部署)同时向数据库申请号段时,如何保证不拿到重复的号段?
-
原理 :利用数据库的
UPDATE语句的原子性。-- 核心 SQL:利用 update 的行锁和返回值
UPDATE leaf_alloc SET max_id = max_id + step WHERE biz_tag = 'order';
或者使用版本号机制(乐观锁):
UPDATE id_generator SET max_id = max_id + step, version = version + 1
WHERE biz_tag = 'order' AND version = old_version;
- 效果:即使有 10 个服务实例同时请求,数据库也能保证只有一个实例的更新成功,从而确保号段的全局唯一性。
| 战术 | 解决的问题 | 核心手段 |
|---|---|---|
| 批量预取 | 数据库压力大 | 一次拿 1000 个,减少 IO 次数 |
| 本地分配 | 生成速度慢 | 内存自增,无网络开销 |
| 双缓冲 | 申请时阻塞 | 80% 时异步预加载,无缝切换 |
| 乐观锁 | 多实例冲突 | 数据库行锁/版本号,确保唯一 |
号段模式通过**"数据库做批发,内存做零售"** ,配合**"提前备货(双缓冲)"**,成功将高并发的压力从脆弱的数据库转移到了高性能的本地内存中!
总结:它是怎么工作的?
用一句口诀来概括雪花算法的一生:
"先看时间戳,再查机器号,同毫秒就排队,拼起来拉倒!"
- 时间戳定先后(保证趋势递增)。
- 机器号分你我(保证分布式唯一)。
- 序列号防冲突(保证高并发唯一)。
这就是雪花算法,一个简单、高效、优雅的分布式 ID 生成方案!