想象一座大型快递分拣中心:每天上千万件包裹涌进来,每一件都必须盖一个全中心唯一的编号,才能追踪它从哪条传送带进来、在哪个分区暂存、几点几分被送上哪辆车。最早,中心只有一台发号总机,盖一个号查一次总账,高峰期排起长队;更麻烦的是,如果哪天总机的挂钟被人往回拨了几分钟,新盖的编号甚至可能比旧编号还小,分拣员瞬间就乱了套。后来工程师们想出一个聪明的办法:不再依赖一台总机,而是让每条分拣线自己盖戳——号码里同时写进“到件时间 + 工位编号 + 这一秒的第几件”,全世界不重号、按时间可排序、盖戳极快。这就是分布式 ID 领域著名的 Snowflake 雪花算法,以及把它工程化、解决各种突发问题的美团 Leaf 方案。这篇文章就从快递分拣中心的视角,把整套编号体系拆给你看。
掌握 Snowflake 与 Leaf 的原理与取舍,你就能在高并发、分库分表的系统里设计出既唯一又可排序的主键,这是分布式架构师的必修基本功。
可视化演示
展示Snowflake雪花算法的64位ID结构(符号1位+时间戳41位+机器ID 10位+序列号12位)与生成流程,包括时钟回拨处理策略和Leaf号段模式对比。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Snowflake 64位ID结构 时间戳41位:~69年 机器ID 10位:1024节点 ID=ts<<22|wid<<12|seq 每ms 4096个ID
为什么需要分布式ID
分拣中心刚开张时只有一间仓库、一个老大爷拿本子记编号,一天几百件,绰绰有余。可随着包裹量暴涨,仓库分成了三个区,每个区各记各的号,结果 A 区和 B 区在同一天都盖出了"000123"——两件包裹编号相同,追踪时根本分不清谁是谁。分布式系统面临一模一样的问题:业务量上来后,数据库被拆成多张分片表,每个分片各自维护自增主键,不同分片之间必然撞号。
从技术角度看,分布式 ID 有四个典型的需求场景。第一是分片主键:分库分表后,主键必须全局唯一,否则数据定位会错乱。第二是幂等控制:消息队列可能重试投递,支付回调可能重复推送,系统需要用一个唯一的业务 ID 判断“这条请求我处理过没有”。第三是链路追踪:一次用户请求会穿越几十个微服务,日志里没有 traceId 就拼不出一条完整的调用链。第四是可追溯性:订单、交易等核心数据要支持审计,从 ID 能反推发生时间,是合规审计的硬要求。
设计编号规则前,先要回答一个问题:未来几年需要多少个号?这决定了编号要留多少位。假设未来每天的包裹量是 N,那么编号空间至少要容纳 N 个不同的值。
回到分拣中心:如果预估五年后包裹量达到十亿件,那么 2^30 ≈ 10.7 亿,编号至少要 30 位。这就是为什么所有分布式 ID 方案在设计时,都要先认真估算业务增长,给时间戳和序列号留足位数。
- 位的表示能力一个二进制位有 0 和 1 两种状态,b 个位可以组合出 2^b 个不同的编号。
- 编号总数需求要让 N 个包裹都能拿到不同的号,编号空间必须不小于 N。
- 取对数并向上取整两边取以 2 为底的对数得到 b ≥ log₂N,由于位数必须是整数,再向上取整。
ID设计三大原则
分拣员心里有三把尺子来评判编号方案好不好:第一,号不能重——盖重了包裹就“身份混淆”了;第二,按号能看出先后——分拣员扫一眼编号就知道哪件先到;第三,盖章不能拖慢流水线——为了盖个号让传送带停两秒,谁也受不了。这三把尺子放到分布式系统里,就是分布式 ID 的三大设计原则:全局唯一、趋势递增、高性能高可用。
所谓全局唯一,是说在任何时刻、任何节点生成的 ID 都不能重复。实现上通常把“时间”和“空间”拆开:时间维度用时间戳保证不同时刻的号不同,空间维度用机器 ID 保证同一时刻不同机器不同号。所谓趋势递增,是说 ID 的数值大体上随着时间推移而变大,这样数据库的 B+ 树索引能像往末尾追加一样顺序写入,避免随机插入导致的页分裂;但它并不要求每一毫秒都严格单调。
至于高性能高可用,要求 ID 生成在内存中完成、微秒级返回,不能每次生成都去捅一下数据库;同时发号服务不能成为新的单点,挂了不能影响主流程。最后还有一条容易被忽视的增强项——信息安全性:订单号如果连续自增,竞争对手每天下单两次就能算出你的日销量,所以电商场景往往要求 ID 不可枚举、无法推测。
这三把尺子,就是接下来评审所有方案的标准。任何方案好不好,拿尺子一量便知。
- 时刻先后任意两次发号时刻 t₁、t₂,t₁ 更早发生。
- 编号同步变大早发生的请求拿到的 ID 数值更小,后发生的更大。
- 趋势而非严格公式刻画的是“整体随时间的单调性”,个别异常时刻(时钟回拨)允许小幅倒退,不会因为一次回退就推翻方案。
常见方案对比
在 Snowflake 出现之前,分拣中心尝试过四种编号法。最省事的是“手工大本子”:每个库房拿一本厚册子,自己翻页抄号,抄到重复的概率极低——这就是 UUID。最经典的是“总机盖戳”:所有库房共用一台发号总机,盖一个号记一次账——这就是数据库自增。后来总机顶不住压力,改成“按天领号源”:每天从总台领一批号用——这就是 Redis 自增。再到最后一招:“提前领一摞号”:一次领一万个号存在库房慢慢用——这就是号段模式。
UUID 是 128 位随机数,客户端本地就能生成,全球不重号。但它的致命伤是“无序”:库房本子上先写 7fe2 再写 1a9c,数据库主键索引的插入顺序完全是随机的,百万行之后性能明显劣化。数据库自增 ID 简单且严格递增,但所有请求都得打到同一台主库,QPS 上不去,一旦分库还会跨库撞号。Redis 自增用 INCR 命令扛并发,却引入了新的中间件依赖,Redis 一旦抖动或丢数据,ID 就可能出问题。号段模式最接近现代方案:一次申请一批,段内本地 +1,数据库压力骤降。
UUID 的随机性让人以为它“几乎不重复”,但它的代价是 128 位的存储空间和完全无序的索引行为:
四种方法一个比一个“离总机更远”,从完全依赖中心,到提前把号搬到本地。最后一种“领一摞号”的思路,正是美团 Leaf 号段模式的原型。不过要真正落地,还需要解决“号快用完时去哪领”的细节,我们留到 Leaf 一节细说。
- 两种编号的存储单位UUID 是 128 位(16 字节),Snowflake/自增 ID 是 64 位(8 字节)。
- 空间开销的倍数两者相除得到存储成本恰好是两倍。
- 索引连锁反应InnoDB 聚簇索引按主键大小组织,更大且无序的主键会让索引膨胀、页分裂增多。
Snowflake算法原理
雪花算法 Snowflake 由 Twitter 提出,是分布式 ID 领域最经典的 64 位设计方案。回到分拣中心的比喻:它不再让总机挨个盖戳,而是给每条分拣线发一个固定的“工位编号”(机器 ID),每个包裹的电子戳由三部分组成——包裹到达的毫秒时间、这条分拣线的工位号、这一毫秒内这是第几件。三个信息拼在一起,全世界不会重号,而且扫一眼时间戳部分就知道包裹什么时候到的。
Snowflake 把 64 位整数切成了四段:最高 1 位固定为 0(保证 ID 是正数);接着 41 位是毫秒时间戳,记录“当前时间 - 纪元起点”的差值;再接着 10 位是机器 ID,最多支持 1024 台机器(每条分拣线一个号);最后 12 位是序列号,表示同一毫秒内该机器生成的第几个 ID,范围 0~4095。整体结构可以写成下面的位运算表达式:
看到这里,你可能会问:41 位时间戳 + 10 位机器号 + 12 位序号,容量够吗?算一笔账:单台机器每毫秒能生成 4096 个 ID,1024 台机器合起来每秒理论上限超过 42 亿——这远远超出了绝大多数业务的峰值。而且 ID 的数值大小完全由时间戳主导,天然满足“趋势递增”,数据库插入接近顺序写。这就是为什么 Snowflake 能成为分布式 ID 的事实标准:位分配优雅,性能极高,几乎零依赖。
- 时间戳段左移 22 位T 是当前毫秒时间,T₀ 是纪元起点(如 2010-11-04),差值占 41 位;左移 22 位把这段放到高位。
- 机器号左移 12 位M 是工位编号(0~1023),左移 12 位后恰好落在时间戳段之后的 10 位。
- 按位或完成拼接三段做按位或,低 12 位留给序列号 S,三部分互不重叠,拼成一个 64 位整数。
- 时间容量校验41 位毫秒能表示约 2.2 万亿毫秒,约为 69.7 年,从纪元起算足够业务使用几十年。
时钟回拨的应对
想象一个场景:分拣中心的墙上挂钟是自动对时的。某天 NTP 校时发现挂钟快了 3 秒,于是悄悄把钟往回拨了 3 秒。这时问题来了:刚盖完戳的包裹显示 10:00:05,下一个包裹却盖上 10:00:02——时间倒退了,后面的编号数值反而比前面的小,甚至可能和 3 秒前盖的号完全重复。在分拣中心,这意味着两个包裹贴上了同一个编号。
在分布式系统里,服务器的系统时钟同样受 NTP 校时、运维人员手动调整的影响,可能发生回拨。Snowflake 的时间戳段完全依赖本地时钟,回拨一旦发生,最直接的后果就是“新 ID 的时间戳位点倒退”,破坏趋势递增,极端情况下生成重复 ID。所以任何生产级的 Snowflake 实现,都必须记录上一次生成 ID 的时刻(简称位点),并在每次发号前检查:
但“等一等”不是万能药。如果回拨只有几毫秒,等待几毫秒无伤大雅;如果回拨了几秒甚至几分钟,让接口阻塞这么久显然不可接受。生产环境的做法通常是分层应对:小回拨(如 <5ms)等待追平;中等回拨用上次位点兜底,暂时用备用发号策略;大回拨直接拒绝服务并报警,宁可短时间不可用,也不能发重复 ID。这就好比挂钟被拨慢了,先停下来等钟走准,如果钟坏得太离谱,就干脆贴出“暂停取号”的告示,通知维修工。
- 记录上次位点每次成功生成 ID 后,把当时的毫秒时间保存为 T_last。
- 计算回拨量用上次位点减去当前时间,得到差值 Δ。
- 判定与等待Δ > 0 说明发生回拨,线程休眠 Δ 毫秒,让时钟追平后再发号,保证新时间戳不小于旧时间戳。
美团Leaf实现
美团点评的开源组件 Leaf,把 Snowflake 从“算法”变成了“生产可用的发号器”。它提供两种模式,对应分拣中心的两种管理风格。第一种叫号段模式(Leaf-segment):总部不再给每条分拣线发单独的工位号,而是直接发“一摞号”。比如订单业务这一摞是 100001~110000,分拣线在本地一个号一个号地盖,盖到接近用完时,再去总部领下一摞。这样数据库只需要在“领号”时被访问一次,而不是每盖一个戳就查一次账。
号段模式的核心状态都存在数据库的一张表里:业务标识(biz_tag)、当前已发到的最大号(max_id)、步长(step)。每次申请新号段时,用乐观锁把 max_id 向前推进 step:
单靠号段还有一个隐患:万一号段用尽的那一刻数据库刚好抖动,发号服务就卡死了。Leaf 用“双 Buffer”解决这个问题——同时维护两个号段 buffer,一个在用、一个事先预加载好;当前者剩余量低于阈值(比如 10%)时,后台立刻异步去数据库领取下一段填进空闲 buffer。两个 buffer 交替供号,任何时刻都保证有一摞“备好的号”可用,号段切换对调用方完全无感。
第二种模式叫雪花模式(Leaf-snowflake),思路就是原版 Snowflake 加上“工位编号注册表”:每台机器启动时,到 Zookeeper 上注册一个持久顺序节点,拿到一个全局唯一的机器 ID;Zookeeper 相当于分拣总部的登记簿,谁领过几号工位都记得清清楚楚,机器宕机后节点还能自动回收。更妙的是,Leaf 还会把上次发号时间戳写入 Zookeeper,一旦发现时钟回拨到早于登记的时间,立即拒绝启动或报警,把“回拨风险”挡在服务上线之前。这样一来,号段模式适合中小规模、依赖数据库的场景;雪花模式适合超大规模、对时钟和基础设施有要求的场景。两者互补,构成了一张完整的分布式 ID 解决方案地图。
- 当前水位数据库里记录该业务已经分配到的最大号 cur_max_id。
- 一次性推进新申请时把水位向后推进 step 个号,得到新的水位。
- 段内自增本次可用的号是左开右闭区间,段内每次加一,在本地内存直接完成。
选型与落地
选编号方案就像分拣中心按规模选设备:小镇网点一天几百件,一个大本子就够;省会枢纽一天几百万件,必须上自动分拣线。分布式系统选 ID 方案,第一件事就是评估业务量。峰值 QPS 是万级还是百万级?是否要求 ID 按时间排序?现有的基础设施里有没有高可用的数据库或 Zookeeper?把这些问题答完,方案基本就浮出水面了。
如果选择号段模式,一个关键的调优参数是 step 的大小。step 太小,号段很快耗尽,频繁访问数据库;step 太大,单段内号码浪费的风险增加。step 的下限可以用下面的式子估算:
落地时还有两个隐形成本要算进去。第一是时钟可信度:云服务器、容器环境的时钟可能因 NTP 频繁跳变,如果团队没有能力做时钟监控,选强依赖本地时钟的雪花模式要格外谨慎。第二是运维成本:自研一个 Snowflake 要自己处理机器 ID 分配、时钟回拨、高可用,而引入 Leaf 或者商业组件则要接受它的架构约束和依赖组件(Zookeeper/MySQL)。没有“最好的方案”,只有“当前团队和业务最合适的方案”——这正是架构师的价值所在。
回到分拣中心:从手工大本子,到总机盖戳,再到按工位分布式盖戳,每一步演进都是在“唯一性、顺序性、性能、成本”四个维度上的权衡。Snowflake 给出了优雅的位分配框架,Leaf 把它打磨成了不怕时钟回拨、不怕数据库抖动的工程化产品。理解了这条演进脉络,下次面试官问起“你们系统的订单号怎么生成”,你就能从快递分拣中心的故事开始,讲出一套完整的架构决策。
- 号段耗尽时间step 个号在峰值流量下能撑的时间是 step / QPS_peak。
- 补给耗时号段耗尽后,去数据库领下一段需要 T_refill(网络往返 + 数据库事务)。
- 不阻塞条件撑的时间必须大于等于补给耗时,移项得到 step 的下限。
AI 应用场景
电商订单号生成
阶段:高并发数据写入与分片存储设计
解决的问题:电商订单表按用户维度分库分表后,每个分片的自增主键会互相撞号;传统方案把订单号设计成“时间+随机数”,既难保证唯一又无法按时间排序。Snowflake/Leaf 生成的订单号全局唯一、趋势递增,数据库索引顺序写入,写入性能大幅提升。
具体案例:某外卖平台订单中心使用 Leaf 号段模式生成订单号,高峰期 QPS 数十万。运营人员在工单系统里看到订单号,一眼就能读出下单时间,故障排查时还能按订单号的时间戳段快速圈定异常时段。
消息队列幂等去重
阶段:数据管道一致性保障
解决的问题:消息队列普遍采用 at-least-once 投递语义,消费者可能收到重复消息。给每条消息分配全局唯一 ID,消费端用 ID 做去重表主键,重复消息直接丢弃,是解决“重复入账”“重复发券”等数据一致性问题的标准手段。
具体案例:金融对账系统中,支付结果通知可能被重复投递多次。平台用 Snowflake 生成消息 ID,消费端在 Redis 里记录已消费 ID 集合,新消息先查重再处理,同一笔支付成功通知即使到达三次,也只入账一次。
微服务全链路追踪
阶段:可观测性建设与排障
解决的问题:一次请求穿越数十个微服务,日志散落在几百台机器上,没有全局唯一 ID 就无法串联。用 Snowflake 生成的 traceId 不仅全局唯一,还自带时间信息和机器信息,排障时能按时间窗口和机器维度双重定位。
具体案例:某网约车平台在一次晚高峰事故中,SRE 团队用 traceId 的时间戳位段快速锁定 20:30-20:45 的异常请求批次,再按机器 ID 位段定位到出问题的机房和实例,把故障定位时间从小时级压缩到分钟级。
Python 演示
import threadingimport time class Snowflake: def __init__(self, machine_id, epoch=1700000000000): if not 0 <= machine_id < 1024: raise ValueError("machine_id must be in [0, 1024)") self.machine_id = machine_id self.epoch = epoch self.seq = 0 self.last_ts = -1 self.lock = threading.Lock() def gen_id(self): with self.lock: ts = self._next_timestamp() if ts == self.last_ts: self.seq = (self.seq + 1) & 4095 if self.seq == 0: while ts <= self.last_ts: ts = int(time.time() * 1000) else: self.seq = 0 self.last_ts = ts return ((ts - self.epoch) << 22) | (self.machine_id << 12) | self.seq def _next_timestamp(self): ts = int(time.time() * 1000) if ts < self.last_ts: print("clock rollback detected, waiting...") time.sleep((self.last_ts - ts) / 1000.0) ts = int(time.time() * 1000) return ts def parse_id(uid, epoch=1700000000000): seq = uid & 4095 machine = (uid >> 12) & 1023 ts = (uid >> 22) + epoch return ts, machine, seq if __name__ == "__main__": sf = Snowflake(machine_id=7) uids = [sf.gen_id() for _ in range(2000)] print("total:", len(uids)) print("unique:", len(set(uids))) print("strictly increasing:", all(uids[i] < uids[i + 1] for i in range(len(uids) - 1))) print("parse first:", parse_id(uids[0]))
self.seq = (self.seq + 1) & 4095序列号是 12 位,加到 4096 时与 4095 做按位与自动归零,等价于% 4096但更快。return ((ts - self.epoch) << 22) | (self.machine_id << 12) | self.seq三段位运算拼接,正是正文中 Snowflake 核心公式的落地。if ts < self.last_ts:发号前检测时钟回拨,发现当前毫秒时间小于上次发号时间就进入等待逻辑。time.sleep((self.last_ts - ts) / 1000.0)睡到时钟追平上次位点,避免生成时间倒退的 ID。uids[i] < uids[i + 1]用严格递增来观察“趋势递增”在正常时钟下的表现,这个性质依赖稳定的系统时钟。
常见误区
误区“Snowflake 生成的 ID 是严格递增的,所以可以直接拿它当排序字段,绝对不乱序。”
正解Snowflake 只能保证“趋势递增”。同一毫秒内序列号确实递增,跨毫秒时新毫秒的序列号从 0 重新开始,只是时间戳更高所以整体仍变大;一旦发生时钟回拨,新 ID 的数值可能小于旧 ID。依赖严格单调的业务(如凭 ID 判断事件先后)需要额外校验,不能把趋势当严格。
为何常见:在单机、时钟稳定的环境下连续生成,肉眼看到的现象就是递增的,“趋势”与“严格”的差异只有遇到回拨或跨节点才会暴露。
误区“反正有 10 位机器号,最多 1024 台机器,随便配一个数就行,撞了再改。”
正解10 位机器 ID 只有 1024 个取值,一旦两台机器配置了相同的 machine_id,又恰好在同一毫秒生成 ID,序列号也可能相同,两个 ID 就完全一样。机器 ID 必须通过 Zookeeper 注册中心或配置管理系统统一分配,保证全局唯一,不能靠“随手填”。
为何常见:本地开发时单机单进程永远撞不上,很容易忽略多机部署时机器号重复的隐患。
误区“时钟回拨时让生成线程睡一会儿,等时钟走回来就能解决所有问题。”
正解等待策略只适合毫秒级的小回拨。回拨幅度大时(NTP 把时钟拨回几秒甚至几分钟),让线程阻塞等待会让接口延迟彻底失控,大量请求堆积。生产级方案通常叠加多种手段:记录上次位点、回拨超阈值时拒绝服务并报警、或者切换到备用发号器。
为何常见:很多教程示例只演示了 sleep 等待这一种策略,读者容易把“最简单”当成“最完整”。
误区“Leaf 号段模式本质还是数据库自增,每次发号都要写库,性能没区别。”
正解两者访问数据库的频率天差地别。自增 ID 每个号写一次库,Leaf 号段模式一批号(如 10000 个)写一次库,数据库压力下降上万倍;双 Buffer 还会在号段用尽前提前异步加载下一段,发号过程完全不阻塞。
为何常见:从外部看两者都是“发一个号数字加一”,容易忽略底层“逐条写库”与“批量领段、本地发号”的巨大差异。
误区“UUID 全球唯一、不用配置机器 ID,用它当主键更省事,为什么要用 Snowflake?”
正解UUID 无序且占用 16 字节。作为 InnoDB 聚簇索引主键时,随机值导致页分裂和索引碎片化,写入性能明显劣化;Snowflake 是 64 位 BigInt、趋势递增,索引插入接近顺序写,空间也更省。数据量小的时候看不出差别,到了亿级行、高并发写入就高下立判。
为何常见:小数据量下 UUID 的缺点完全感知不到,容易让“快”的直觉掩盖“慢”的真相。
面试题
Q1. Snowflake 生成的 64 位 ID 中,时间戳字段通常占用多少位?
- A.12 位
- B.41 位← 正确答案
- C.10 位
- D.64 位
答案:B。Snowflake 的 64 位布局是 1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号。12 位是序列号,10 位是机器号,41 位才是时间戳,它能表示约 69 年的毫秒时间。
Q2. 美团 Leaf 的雪花模式中,机器 ID 的全局唯一分配依赖哪个组件?
- A.Zookeeper 注册中心← 正确答案
- B.Redis 缓存服务
- C.Kafka 消息队列
- D.Nginx 反向代理
答案:A。Leaf 使用 Zookeeper 的持久顺序节点为每台机器分配唯一 ID,机器宕机后临时节点自动消失、可回收再用。Redis 只做缓存、Kafka 是消息队列、Nginx 是网关,都不承担机器号分配职责。
Q3. 为什么 Snowflake 能支撑大约 69 年的 ID 生成?
- A.序列号有 12 位
- B.41 位毫秒时间戳← 正确答案
- C.机器 ID 有 10 位
- D.ID 是 64 位整数
答案:B。2^41 毫秒约等于 69.7 年,即 41 位时间戳从纪元起点开始最多数 69 年才会溢出。序列号位数影响并发能力,机器号位数影响部署规模,总位数决定存储大小,而“能用多少年”只由时间戳位数决定。
Q4. 下列哪些属于分布式 ID 的典型设计原则?
- A.全局唯一不重复← 正确答案
- B.趋势递增可排序← 正确答案
- C.高性能高可用← 正确答案
- D.严格单调无回退
答案:ABC。全局唯一、趋势递增、高性能高可用是三大原则。D 错在“严格单调”——Snowflake 在时钟回拨时可能短暂回退,只能保证趋势递增,不能承诺严格单调,这也是设计上对现实时钟的妥协。
Q5. 某 Leaf 号段配置 step=10000,峰值 QPS=50000,平均多久去数据库申请一次新号段?
- A.每 0.2 秒← 正确答案
- B.每 2 秒
- C.每 20 秒
- D.每 200 秒
答案:A。10000 ÷ 50000 = 0.2 秒,即每 0.2 秒就要领一次新号段。这个频率说明号段模式下数据库访问频率取决于 step 与 QPS 的比值,也解释了为什么 Leaf 要用双 Buffer 提前预加载,避免 0.2 秒一次的领号造成阻塞。
Q6. 下列关于 Snowflake 时钟回拨的说法,哪一个是正确的?
- A.回拨后必须停机
- B.新 ID 可能小于旧 ID← 正确答案
- C.回拨只影响序列号
- D.回拨由 DNS 抖动导致
答案:B。回拨使当前毫秒时间变小,时间戳段变低,新 ID 数值可能小于旧 ID,破坏趋势递增甚至引发重复。回拨常见来源是 NTP 校时,等待追平是常见对策,未必需要停机,DNS 抖动与时钟回拨无关。
Q7. 美团 Leaf 双 Buffer 机制解决了哪些问题?
- A.号段用尽时阻塞← 正确答案
- B.频繁访问数据库← 正确答案
- C.号段切换不中断← 正确答案
- D.代替机器 ID 分配
答案:ABC。双 Buffer 同时维护两个号段,一个在用、一个预加载,当前号段剩余低于阈值时后台异步领取下一段。这样号段用尽前已备好下一段,不阻塞、减少数据库访问、切换无感知。机器 ID 的分配由 Zookeeper 负责,与双 Buffer 无关。
Q8. 某 Snowflake 实例上次发号时间 last_ts=1700000000123,当前时间因 NTP 回拨变成 1700000000050,以下哪种处理最合理?
- A.立即继续发号并清零
- B.等待 73ms 后再发号← 正确答案
- C.把 last_ts 改成当前值
- D.重启机器后再发号
答案:B。回拨量 = 1700000000123 - 1700000000050 = 73ms。等待 73ms 让时钟追平,可以保证新 ID 的时间戳不低于 last_ts,避免趋势倒退。立即发号会得到偏小的时间戳,改写 last_ts 会丢失位点,重启则属于过度反应。
Q9. 相比 Snowflake,UUID 做主键最大的劣势是什么?
- A.无序且占用空间大← 正确答案
- B.生成速度比较慢
- C.一定会产生重复
- D.依赖中心服务器
答案:A。UUID 是 128 位随机值,无序会让 InnoDB 聚簇索引随机插入、频繁页分裂;而 Snowflake 是 64 位且趋势递增,索引接近顺序写。UUID 生成速度快、几乎不重复、不需要中心服务器,所以 B/C/D 都不成立。
Q10. 用单库自增 ID 支撑分布式订单系统时,会遇到哪些问题?
- A.单库写入成瓶颈← 正确答案
- B.分库后主键撞车← 正确答案
- C.数据库故障不能发号← 正确答案
- D.ID 天然全局唯一
答案:ABC。自增 ID 依赖单库计数器,所有写入打到一台库,QPS 受限;分库后各库独立自增必然产生重复主键;数据库一旦故障,发号服务直接停摆。D 恰恰相反——自增 ID 在分布式场景下既不全局唯一,也毫无分布式容错能力。