雪花算法:分布式世界的“身份证号”

嘿,朋友!想象一下,你是一家拥有几千台服务器的互联网大厂架构师。现在有个小麻烦:你的订单系统每秒钟要生成几万个订单号。

如果让数据库自己搞(自增ID),几台数据库凑在一起,肯定会出现"撞车"------大家都生成了ID 1001,这就像两个新生儿都叫"张三",户籍科(数据库)当场就得炸毛。

这时候,Twitter 的那帮天才掏出了一个叫 雪花算法 的宝贝。这玩意儿生成的 ID 不长,还是个数字,但就像雪花一样,世界上没有两片完全相同的雪花

它是怎么做到的呢?全靠"拼积木"。


核心原理:把时间、地点、序号拼成"身份证"

雪花算法生成的 ID 是一个 64 位的长整型数字 (在 Java 里是 long 类型)。这 64 位并不是随机乱填的,而是像切蛋糕一样,被切成了几块,每一块都有特殊的含义。

我们可以把它看作一张**"数字身份证"**:

积木块 位数 含义 通俗解释
符号位 1 位 固定为 0 我是好人:保证生成的 ID 是正数,别搞成负数吓唬人。
时间戳 41 位 毫秒级时间 出生时间:记录你出生的精确时刻(精确到毫秒)。
机器ID 10 位 数据中心+机器 籍贯/住址:你是北京机房 3 号服务器生的,还是上海机房 1 号生的。
序列号 12 位 自增数字 同毫秒排行:如果同一毫秒生了好几个,那就排号:001, 002, 003...
为什么这样拼就不会重复?

这就好比给新生儿起名:

  1. 时间不同:只要你不是穿越回来的,时间戳肯定不一样,ID 就不一样。
  2. 机器不同:就算时间一样(同一毫秒),你是 1 号机器生的,我是 2 号机器生的,机器 ID 不同,ID 也不一样。
  3. 序号不同:就算时间一样、机器也一样(高并发),那咱们就排号,你是老大,我是老二,序号不同,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 多花好几倍空间。
    • 无序:插入数据库时,索引树会频繁分裂(就像把书随机插进书架,得不停挪位置),写入性能极差。
  • 评价 :除非你不需要存数据库,或者只是用来做临时文件名,否则别拿它当主键
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)才去申请下一段,那么在申请新号段的几百毫秒内,业务线程会被阻塞。为了解决这个问题,号段模式引入了"双缓冲"或"异步预加载"。

  • 原理
    1. 维护两个号段:内存中同时持有"当前号段"和"下一个号段"。
    2. 阈值触发 :当"当前号段"使用到一定比例(例如 80% ,即发到 1800)时,后台异步线程立即去数据库申请下一个号段(2001~3000)并加载到备用缓冲区。
    3. 无缝切换 :当"当前号段"发完(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 生成方案!

相关推荐
AIminminHu3 小时前
OpenGL渲染与几何内核那点事-项目实践理论补充(一-2-(3)-当你的协同CAD服务器面临“千人同屏”时:从单机优化到分布式高并发)
运维·服务器·分布式
真上帝的左手4 小时前
12. 消息队列-RabbitMQ-高可用核心机制
分布式·rabbitmq·java-rabbitmq·mq
枫叶v.4 小时前
Kafka 怎么保证消息的顺序性
分布式·kafka
yitian_hm7 小时前
深入理解 Kafka Producer 核心源码:消息发送全链路解析
分布式·kafka·linq
Dylan~~~17 小时前
深度解析Cassandra:分布式数据库的王者之路
数据库·分布式
传感器与混合集成电路20 小时前
面向储气库注采井的分布式光纤监测技术
分布式
ZTLJQ20 小时前
任务调度的艺术:Python分布式任务系统完全解析
开发语言·分布式·python
被摘下的星星21 小时前
Hadoop伪分布式集群搭建实验原理概要
大数据·hadoop·分布式
无名-CODING1 天前
Java 爬虫高级技术:反反爬策略与分布式爬虫实战
java·分布式·爬虫