号段模式本地缓存为什么能提升性能?
简化版
本地缓存把“每生成一个 ID 访问一次数据库”变成“每消耗一个号段访问一次数据库”,绝大多数请求只做内存原子递增,因此吞吐更高、延迟更稳,也能容忍中心短暂故障。 安全前提是号段由中心原子分配且绝不重叠;未用完的号码宁可浪费,也不能回收给别的实例。
详细版
实例领取 [start,end] 后用 AtomicLong 发号。若步长 100000、实例 5000 ID/s,则一次数据库申请可支撑约 20 秒。双 buffer 在当前段剩余约 10%~20% 时异步预取下一段,使切段不阻塞业务。
预取要 singleflight,避免多个线程重复申请;申请失败按退避重试,并根据 remaining/rate 计算耗尽风险。进程重启丢弃未用区间会产生空洞但不重复。数据库更新 max_id 必须用事务锁/CAS,并防备份恢复造成高水位回退。
完整版教学
一、逐号访问数据库的瓶颈
若每个 ID 都执行 UPDATE ... RETURNING,10 万 ID/s 就意味着 10 万次事务、网络往返和日志写,数据库成为全局串行点。本地号段把中心协调频率降低为 QPS / step,唯一性仍由区间互斥保证。
记忆钩子:中心负责“分地”,实例负责“数地里的门牌号”。
二、基本分配流程
DB max_id=100000, step=10000
atomic update -> max_id=110000
instance owns [100001,110000]
local next(): AtomicLong.incrementAndGet()
两个实例并发申请时,行锁或 CAS 让更新串行,分别拿到不重叠区间。客户端本地发号不再访问数据库。
三、性能提升如何量化
| 步长 | 发号 QPS | 理论申请频率 | 单实例可脱机时间 |
|---|---|---|---|
| 1 | 5000 | 5000/s | 0.2ms |
| 1000 | 5000 | 5/s | 0.2s |
| 100000 | 5000 | 0.05/s | 20s |
步长越大中心压力越低,但重启浪费和低流量业务占用空间越多。要根据峰值、数据库 RTO 和可接受空洞平衡。
四、双 Buffer 消除切段尾延迟
当前 segment 消耗到阈值时,后台线程申请 next segment。当前耗尽后通过原子切换把 next 变 current,再继续预取。若多个请求同时触发,只允许一个加载任务,其他请求仍消费当前段。
current [1..100000] --剩余15%--> async load next [100001..200000]
current exhausted -> atomic swap -> next becomes current
五、故障时怎样处理
数据库不可用但 current/next 尚有余量时继续发号;余量耗尽则停止,不能本地猜一个新区间。申请失败使用有界指数退避,且不能让所有实例同一时刻重试数据库。进程重启后未用段直接废弃,恢复前可持久化段信息,但复用逻辑必须证明没有另一实例接管。
数据库从旧备份恢复时,max_id 可能低于曾分配上界。恢复流程要以独立保存的分配审计高水位为准并向上跳过安全区间;业务最大 ID 不能单独证明安全,因为实例可能仍持有更高但尚未使用的号段。若审计不可用,只能先 fencing 全部旧实例并证明没有未决区间,再选择足够高的新起点。
六、监控与容量预警
核心指标是 current/next 余量、消耗速度、time-to-exhaust、预取耗时/失败、数据库更新冲突、段切换 P99 和浪费率。剩余 20% 并不总安全:低峰可用数小时,高峰可能 2 秒耗尽。
压测要覆盖切段瞬间、DB 中断、预取线程卡死和实例批量重启。唯一约束冲突应作为最高级故障处理。
七、常见误区与追问
- 误区:本地缓存会导致多实例重复。 只要中心区间原子且不重叠,本地消费是安全的。
- 误区:未用完号段应该回收节省号码。 无法证明旧实例已停止时回收会碰撞。
- 误区:步长越大越好。 会增加空洞、低频浪费和容量规划难度。
- 误区:剩余比例能准确表示风险。 还必须结合实时消耗速度计算耗尽时间。
- 追问:双 buffer 如何防重复加载? 用 per-key 状态机或 singleflight 串行预取。
- 追问:DB 挂了能撑多久?
current+next remaining / peakRate给出可解释时间。 - 追问:重启后段还能复用吗? 默认丢弃最安全,复用需持久租约与 fencing 证明所有权。
八、加强记忆
号段缓存的价值来自“低频中心划段、高频内存递增”,双 buffer 把申请延迟藏在当前段消费期内。安全底线是区间绝不重叠、故障耗尽就停发、旧段不回收;容量则用剩余量除峰值速率计算,而不是只看百分比。