新闻详情

新闻详情

首页 / 资讯中心 / 详情

高并发流量治理实战(6):分布式 ID 生成:Snowflake 时钟回拨与号段模式

发布时间:2026/9/28 4:49:45来源:尧图网络
高并发流量治理实战(6):分布式 ID 生成:Snowflake 时钟回拨与号段模式
从读侧回到写侧上一篇把读路径的极端倾斜热点 Key收拾干净了这一篇站到写侧IM 系统消息表拆到 1024 张分片后第一个撞墙的问题朴素得让人意外——一条消息的 ID 从哪来。单机时代答案是AUTO_INCREMENT分片之后每个库各自自增必然撞号。别小看这个发号问题它在大促零点每秒要吐出几十万个号要求全局唯一撞号消息覆盖、趋势递增B 树追加写页分裂可控、不依赖单点发号器自己不能成为新热点、还不能被竞品猜出体量。这一篇把两条主流路线——Snowflake 与号段模式——都用虚拟时钟实现一遍重点踩两个真事故高发区时钟回拨和号段跳号。先说清楚为什么不用另外两条路多主自增步长错开A 库发奇数、B 库发偶数只在库数固定时成立加一个从库要重排步长且每个库的自增值仍是热点行每次插入都更新同一行的计数器innodb_autoinc_lock_mode调来调去也只是缓解。UUID是唯一性无忧了但 128 位随机串做主键意味着InnoDB 聚簇索引彻底随机插入页分裂率暴涨、缓存命中率跳水、每行还要多背 16 字节的索引成本——第 1 篇讲页结构时说过的账这里全额兑现。工程实践里 UUID 只做业务侧幂等键不进主键。Snowflake把 ID 拆成三段位域Snowflake 的 64 位布局41 位毫秒时间戳自定义起点 10 位机器号 12 位序列号。时间戳占大头意味着单机每毫秒最多 4096 个号序列号耗尽就自旋等下一毫秒机器号 10 位意味着最多 1024 个发号节点节点之间靠机器号隔离、互不通信——这是它吞吐高的原因也是它把身家性命押在每台机器时间单调向前这个假设上的原因。真实部署里 NTP 校时、虚拟机热迁移、闰秒调整都可能在任何时刻把时钟往回拨于是有了回拨策略这门学问。用虚拟时钟重放一段带 5 毫秒回拨的发号序列对比宽容冻结时钟借未来毫秒与严格直接拒发两种策略# Snowflake: 41 位毫秒时间戳 10 位机器号 12 位序列号# 虚拟时钟: 一串毫秒增量, 内含一次 -5ms 回拨, 对比宽容/严格两种策略WORKER_BITS,SEQ_BITS10,12MAX_SEQ(1SEQ_BITS)-1WORKER_ID835classSnowflake:def__init__(self,worker_id,tol_ms5,strictFalse):self.worker,self.tol,self.strictworker_id,tol_ms,strict self.last_ms,self.seq-1,0defnext_id(self,now_ms):ifnow_msself.last_ms:# 时钟回拨了backself.last_ms-now_msifself.strictorbackself.tol:returnNone,拒发: 回拨 %dms%back# 报警摘流/停写now_msself.last_ms# 冻结时钟, 向未来借毫秒ifnow_msself.last_ms:self.seq(self.seq1)MAX_SEQifself.seq0:returnNone,等待: 同毫秒序列耗尽# 真实实现自旋到下一毫秒else:self.seq0self.last_msnow_msreturn(now_ms(WORKER_BITSSEQ_BITS))|(self.workerSEQ_BITS)|self.seq,OKdefdecode(i):seqiMAX_SEQ worker(iSEQ_BITS)((1WORKER_BITS)-1)ts_msi(SEQ_BITSWORKER_BITS)returnts_ms,worker,seq drifts[0,0,0,1,1,2,3,-5,5,1,2,2]# 虚拟毫秒增量序列base1767225600000# 固定起点(不读系统时钟)soft,hardSnowflake(WORKER_ID),Snowflake(WORKER_ID,strictTrue)tbase last_soft_idNoneprint(机器号 %d (占 %d 位), 时间戳起点 %d%(WORKER_ID,WORKER_BITS,base))fori,dinenumerate(drifts,1):td sid,smsgsoft.next_id(t)hid,hmsghard.next_id(t)note_ssmsgifsidisNoneelseseq%d%(sidMAX_SEQ)note_hhmsgifhidisNoneelseseq%d%(hidMAX_SEQ)print(请求%02d 时钟偏移%5dms - 宽容: %-14s | 严格: %s%(i,t-base,note_s,note_h))ifsidisnotNone:last_soft_idsid ts,wk,sqdecode(last_soft_id)print(解码最后一个宽容模式 ID: 时间戳%d(%dms) 机器%d 序列%d%(ts,ts-base,wk,sq))运行输出机器号 835 (占 10 位), 时间戳起点 1767225600000 请求01 时钟偏移 0ms - 宽容: seq0 | 严格: seq0 请求02 时钟偏移 0ms - 宽容: seq1 | 严格: seq1 请求03 时钟偏移 0ms - 宽容: seq2 | 严格: seq2 请求04 时钟偏移 1ms - 宽容: seq0 | 严格: seq0 请求05 时钟偏移 2ms - 宽容: seq0 | 严格: seq0 请求06 时钟偏移 4ms - 宽容: seq0 | 严格: seq0 请求07 时钟偏移 7ms - 宽容: seq0 | 严格: seq0 请求08 时钟偏移 2ms - 宽容: seq1 | 严格: 拒发: 回拨 5ms 请求09 时钟偏移 7ms - 宽容: seq2 | 严格: seq1 请求10 时钟偏移 8ms - 宽容: seq0 | 严格: seq0 请求11 时钟偏移 10ms - 宽容: seq0 | 严格: seq0 请求12 时钟偏移 12ms - 宽容: seq0 | 严格: seq0 解码最后一个宽容模式 ID: 时间戳1767225600012(12ms) 机器835 序列0读输出里两行就够了。请求08是全剧核心虚拟时钟从 7ms 被拨回 2ms宽容模式把发号时钟冻结在 7ms、序列号照加——它向未来借了 5 毫秒只要真实时钟在借期内追回来请求09 起 t≥7一个重号都不会有严格模式当场拒发宁可这笔业务失败。请求03则展示了同毫秒连发靠 12 位序列号在同一时间戳里排出 0/1/2。两种策略都成立但适用面不同回拨在毫秒级、且时钟源会自己追回来的场景NTP 渐进校正冻结策略保可用性回拨达秒级以上时区误配、手动改表冻结等于把未来几分钟的号段预支光必须停写摘流——5ms 的容差就是这道分水岭超过它宽容模式自己会转成拒发。生产上还要补一刀机器号必须由注册中心ZooKeeper/etcd分配并带租约两台机器拿到同一个 worker id 时一切时钟策略都是空谈。号段模式把 DB 当批发商另一条路线彻底绕开时钟发号服务一次从数据库批领一段号UPDATE id_alloc SET max_idmax_id1000然后返回区间内存里慢慢批发。它对 DB 的压力从每个号一次交互降到每 1000 号一次交互还天然支持预取——当前段用到水位线就让后台线程把下一段备好发号路径上永远没有同步 RPC。模拟 2500 次发号看 DB 交互次数与重启代价# 号段模式: 一次从 DB 申请 1000 个号, 用满 90% 时预取下一段(此处同步模拟)STEP,PREFETCH_RATIO1000,0.9classDbSim:def__init__(self):self.max_id,self.calls0,0defalloc(self):self.calls1loself.max_id1self.max_idSTEPreturn[lo,self.max_id,0]# [当前指针, 上界, 已用量]dbDbSim()curnext_segNoneissued0for_inrange(2500):ifcurisNone:curnext_segifnext_segelsedb.alloc()next_segNoneprint([%4d] 启用号段 [%4d, %4d] (DB 累计交互 %d 次)%(issued1,cur[0],cur[1],db.calls))issued1cur[0]1cur[2]1ifnext_segisNoneandcur[2]STEP*PREFETCH_RATIO:next_segdb.alloc()print([%4d] 当前段已用 %d/%d - 预取下一段 [%4d, %4d]%(issued,cur[2],STEP,next_seg[0],next_seg[1]))ifcur[0]cur[1]:curNoneprint(共发号 %d 个, DB 交互 %d 次 (逐行自增需要 %d 次)%(issued,db.calls,issued))left0ifcurisNoneelsecur[1]-cur[0]1print(若此刻进程重启: 手上号段还剩 %d 个号作废 - 号段模式的跳号来源%left)运行输出[ 1] 启用号段 [ 1, 1000] (DB 累计交互 1 次) [ 900] 当前段已用 900/1000 - 预取下一段 [1001, 2000] [1001] 启用号段 [1001, 2000] (DB 累计交互 2 次) [1900] 当前段已用 900/1000 - 预取下一段 [2001, 3000] [2001] 启用号段 [2001, 3000] (DB 累计交互 3 次) 共发号 2500 个, DB 交互 3 次 (逐行自增需要 2500 次) 若此刻进程重启: 手上号段还剩 500 个号作废 - 号段模式的跳号来源2500 个号只碰了 3 次数据库且每次换段都无缝衔接预取在第 900 号就完成——这就是批发的杠杆。但最后一行同样诚实每次部署重启手上没用完的号段整段作废。发号服务一天滚动发布两轮、每轮 20 个实例就是 4 万个空洞。ID 只要求唯一不要求连续的业务无所谓但若有用订单号差值估单日单量的外部接口跳号会把商业数据泄露变成商业数据撒谎。对策分两层号段内洗牌段内乱序发号让外人无法从相邻 ID 差推速率或将步长缩到 100 以内换重启损失上限对可用性要求更高的把单 DB 行升级为多机各自领段 段前缀区分号段服务本身无状态化。选型与落地清单消息/时间强相关、需要 ID 可反解时间对账、排序、分页游标Snowflakeworker id 走注册中心租约回拨容差 5ms 左右超阈值拒发告警订单号要短、要发给外部、对 DB 压力敏感号段模式步长按重启可接受的空洞数倒推段内乱序发号防推断两条路线都可以再加一层号段中间件混合使用Snowflake 的位段里塞机器号换成号段服务器编号兼得趋势递增与可回收的节点管理无论哪种ID 一旦生成就不要再给业务赋予数值语义用 ID 比大小排时间在新旧系统交替时会失效写侧的发号问题解决了但读侧和写侧的交界处还压着一个老问题数据先写库、缓存在什么时候删为什么先删缓存再写库和先写库再删缓存都不对要延迟双删以及当这些招都失效时如何靠 Binlog 把不一致的窟窿兜住下一篇《高并发流量治理实战7缓存一致性工程延迟双删与 Binlog 对账的实现》正面清算这笔账。参考来源Wikipedia: Snowflake ID: https://en.wikipedia.org/wiki/Snowflake_IDTwitter/snowflake 设计文档(GitHub 归档仓库): https://github.com/twitter/snowflakeRFC 4122: Universally Unique Identifiers (UUID): https://datatracker.ietf.org/doc/html/rfc4122MySQL 8.0 Reference Manual: InnoDB Auto-Increment Handling: https://dev.mysql.com/doc/refman/8.0/en/innodb-auto-increment-handling.htmlWikipedia: Universally unique identifier: https://en.wikipedia.org/wiki/Universally_unique_identifier本系列已结集为免费专栏高并发流量治理实战从限流到全链路压测进阶推荐付费专栏提示词工程实战从入门到生产级 Prompt 设计限时 ¥19.9首篇免费试读
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

rust-analyzer failed to load workspace 问题处理:从 settings.json 到 TaoToken 配置排查 2026/9/28 5:52:06

rust-analyzer failed to load workspace 问题处理:从 settings.json 到 TaoToken 配置排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
RVM多输入多输出回归:稀疏贝叶斯原理与MATLAB实战 2026/9/28 5:51:59

RVM多输入多输出回归:稀疏贝叶斯原理与MATLAB实战

简介:基于相关向量机(RVM)的多输入多输出回归分析MATLAB实现,面向本科及以上需要处理小样本、非线性多目标回归问题的学生、研究者和工程师。相关向量机具备稀疏性好、泛化能力强且可输出概率预测的特点,适用于能源、环…

阅读更多 →
排序算法与时间复杂度全解析:从O(n²)到O(nlogn)的选型实战 2026/9/28 5:51:59

排序算法与时间复杂度全解析:从O(n²)到O(nlogn)的选型实战

排序这块内容,几乎是每个学编程的人绕不开的一道坎。不管是校招笔试、面试八股、日常业务开发,还是数据结构和算法的系统学习,排序永远会占据一个特殊的位置。但这个专栏标题里最值钱的部分其实是后面那个括号——“包括时间复杂度讲解”。因…

阅读更多 →
8843张YOLO-ready鱼类图像数据集:XML转YOLOv8实战指南 2026/9/28 5:51:59

8843张YOLO-ready鱼类图像数据集:XML转YOLOv8实战指南

简介:本资源是面向计算机视觉开发者与深度学习初学者的高实用性鱼类目标检测数据集,专为YOLO系列算法(v5/v7/v8/v9/v10/v11)训练、验证与测试设计,解决水下生物识别、水产养殖智能监控等实际场景中的目标检测建模需求。…

阅读更多 →
重庆的网络优化公司对比评测 2026/9/28 5:51:59

重庆的网络优化公司对比评测

3家重庆网络优化公司实测:防黑保姆级建站教程 上周凌晨三点,我盯着服务器监控屏幕,冷汗直冒。某客户的外贸站突然满屏弹窗,全是博彩和假药广告,后台密码也被改了。这就是典型的网站被黑挂马,很多老板这时候才慌,找遍全网找不到靠谱救援。其实这事儿不…

阅读更多 →
PyTorch优化器参数更新步骤全解析:从SGD到AdamW的实战指南 2026/9/28 5:51:46

PyTorch优化器参数更新步骤全解析:从SGD到AdamW的实战指南

新手在 PyTorch 里写完模型定义,卡住的第一个地方往往是:优化器到底是怎么把损失函数变成参数更新的?说实话,我刚接触的时候也疑惑过zero_grad()是不是多此一举,Adam 的参数更新为什么内部还要分好几个步骤。这篇内容就…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉