双 Buffer 机制
双 Buffer 是号段模式的核心优化,提供零等待的 ID 生成体验。
号段模式把取号从”每个 ID 一次”变成”每段一次”,但同步访问发号服务的那一步并没有消失。当本地号段正好用完,下一个请求必须等这次网络往返完成才能拿到新号段,这段空窗期会以延迟尖刺的形式出现在监控曲线上。步长调得越大,空窗期出现得越少,可一旦出现,它就是一次实实在在的接口卡顿。
双 Buffer 的思路是把号段分成”当前使用”和”预加载”两份:Buffer 1 还在发放的时候,后台线程已经替你去申请 Buffer 3;等 Buffer 1 见底,直接切到早已就位的 Buffer 2,切换过程只剩一次内存里的指针交换。这一章要讲清楚的是:步长取多大才够用(步长大则申请少、浪费多,步长小则浪费少、续租频繁)、后台预加载失败时如何兜底、以及 Buffer 使用率要盯到什么程度才算健康。
读完这一章,你应该能画出双 Buffer 的切换时序,说清”零等待”究竟零在哪一步,并结合自己的 QPS 和可接受的 ID 空洞量,给出一套具体的步长与预加载配置。
双 Buffer 解决的是”取号慢”,解决不了”取不到号”——发号服务背后的数据库一旦出问题,号段模式仍然会整体停发。下一节的高可用设计,正是建立在这一节之上。
1. 基本 Buffer
Buffer: [1000, 2000]
分配:1001, 1002, 1003, ...
用完:需要获取新 Buffer
2. 双 Buffer
Buffer 1: [1000, 2000] ← 当前使用
Buffer 2: [2000, 3000] ← 预加载
Buffer 1 用完 → 切换到 Buffer 2
同时预加载 Buffer 3
Java 实现
双 Buffer 在代码里就是两个存放号段的引用加一个后台预加载任务,切换靠一次原子替换完成。
字段:
current 当前正在发放的号段
standby 预加载好的备用号段
preloading 标记,保证同一时刻只有一个预加载任务在跑
取号:
1. 从 current 取号并把它的剩余量减一
2. 剩余量见底时,尝试把 current 换成 standby
3. 换成功 → 立刻发起下一个号段的预加载
换失败 → 说明别的线程已经换过了,直接重读 current
4. standby 为空且预加载还没回来 → 退化为同步取号,本次请求会等待
预加载:
1. 向发号服务申请一个新号段
2. 成功后写入 standby
3. 失败则保留原有 standby 并重试,不影响当前号段的发放
零等待
单 Buffer:
分配 1000 个 ID
Buffer 用完
等待获取新 Buffer(100ms)
继续分配
双 Buffer:
分配 1000 个 ID
Buffer 用完
立即切换(< 1ms)
继续分配
性能提升
单 Buffer:100,000 QPS
双 Buffer:10,000,000 QPS
性能提升:100x
1. 合理设置步长
步长是号段模式最关键的一个参数:它决定一次申请多少个 ID,也决定了号段切换的频率。步长太小,取号频繁,数据库压力大,切换带来的抖动被放大;步长太大,数据库压力下降,但实例异常退出时这一整段 ID 全部作废,ID 空洞成倍放大。
以单实例 10,000 QPS 为例,步长 1,000 意味着每 100ms 就要取一次号段;步长 10,000 则每秒取一次,切换频率下降 10 倍,代价是最坏情况下浪费接近 10,000 个 ID。
常见做法是按业务类型设置不同步长:写入量大的核心业务用大步长换低数据库压力,量小但要求 ID 连续的业务用小步长换少浪费。步长还要留出余量,避免业务量小幅上涨就把切换频率推到压垮数据库的程度。
2. 后台预加载
预加载的核心是”不让请求线程等”。请求线程只负责从当前 Buffer 取号,发现快见底时发起一个异步预加载请求,然后继续发放;真正的取号动作由后台线程完成,拿到的新号段写进备用槽位。
切换时完全不碰网络:当前 Buffer 取空后,直接把备用槽位提升为当前 Buffer,耗时只有一次指针交换(< 1ms),而单 Buffer 每次都要同步取号(约 100ms),两者相差的就是这一整段等待。
预加载必须做好三件事:同一时刻只允许一个预加载任务在跑,否则会重复申请、浪费号段;预加载失败要保留当前 Buffer 的剩余量并重试,不能让请求线程陪着一起失败;预加载线程的异常要单独告警,因为它不会体现在业务错误率上。
3. 监控 Buffer 使用率
Buffer 使用率 = 当前 Buffer 剩余量 / 步长,是双 Buffer 最重要的业务指标。它同时反映两件事:流量是否超出了步长的预期,以及预加载是否跟得上消耗速度。
| 观测现象 | 说明 | 处置 |
|---|---|---|
| 使用率稳定下降,切换后回到满值 | 正常,双 Buffer 运转良好 | 保持当前配置 |
| 使用率长期低于 10% | 消耗速度超过预加载速度,或步长偏小 | 调大步长,检查预加载耗时 |
| 切换耗时从 < 1ms 明显升高 | 预加载没能在切换前完成,切换退化为等待 | 检查数据库连接与网络 |
| 预加载失败次数不为 0 | 发号服务或数据库异常 | 触发告警,必要时降级 |
| ID 空洞数量突增 | 实例异常退出,未发放的号段被丢弃 | 排查实例重启与 OOM |
了解了双 Buffer 机制后,我们学习高可用设计。
👉 下一节:高可用设计
发现内容有问题?
告诉我们哪里需要改进。
