连载中 8/15

热点库存:一万个人抢同一行

2026-09-06 · 289 阅读 · 0 评论 · 0 赞

单 Key 的天花板

Redis 单线程模型是原子性的来源,也是热点的天花板:所有命令排队执行,单个实例十万级 QPS——听起来够,但秒杀的请求全挤在一个 key 上,一个 key 的处理速度就是系统的总吞吐。开篇事故里数据库热点行的问题,换到 Redis 只是从"必死"变成"逼近极限"——十万请求打一个 key,排队延迟肉眼可见地涨。热点要拆,这是唯一出路。

库存分片:一个热点拆成 N 个桶

把一个库存 key 拆成 N 个分片桶:总库存 1000 拆成 10 个桶各 100,扣减时按随机路由选桶执行 Lua,单 key 的压力变成 N 分之一,总吞吐直接乘 N。选桶策略要随机或轮询——按用户 ID 哈希选桶会让"热门用户"永远撞同一个桶。扣减失败(桶空了)要换下一个桶重试,直到所有桶都空才算售罄。

// 分桶扣减:随机起点轮询,桶空换下一个
public long deduct(String skuId, int num) {
    int start = ThreadLocalRandom.current().nextInt(BUCKET_N);
    for (int i = 0; i < BUCKET_N; i++) {
        String key = "stock:" + skuId + ":" + ((start + i) % BUCKET_N);
        long r = redis.eval(DEDUCT_LUA, key, String.valueOf(num));
        if (r >= 0) return r;               // 当前桶扣减成功
    }
    return -2;                              // 所有桶都空:售罄
}
// 预热时:总库存 1000 -> 十个桶各 SET 100

桶不平衡:分片的经典副作用

随机路由不保证均匀:活动结束时可能出现 3 号桶早卖光、7 号桶还剩 40 个——库存没卖完,但一部分用户却被判了售罄,这就是少卖。缓解手段:换桶重试本身就是第一道平衡;再进一步,扣减失败的请求顺手把"空桶"状态记录下来,后续请求跳过已知空桶;规模更大的场景做二次再平衡——某桶余量过少时异步把它合并进其他桶。

售罄后的快速失败

秒杀最有意思的时刻是售罄那一秒:库存归零后,洪峰还在持续涌入——明知会失败,还是来了几万次。这时 Redis 压力已经毫无意义,本地缓存接力:应用内存里维护售罄标记,收到 Redis 返回售罄后置位,后续请求在应用层直接拒绝——连 Redis 都不用碰。标记要有失效机制(回补库存后清除),标记粒度到 SKU 级即可。

热点探测与动态分桶

分多少桶?拍脑袋 10 个可能不够也可能浪费。热点探测来自历史数据与预热监控:历史秒杀的 QPS 曲线、收藏加购数预估这次的热度,预期越高分桶越多;开抢后观察单桶 QPS,逼近阈值可以动态追加桶(从总库存里切份额给新桶)。热点探测这件事,本质是把"这次会多热"从玄学变成可配置的参数。资格扣完了,订单还没影——下一篇讲MQ 异步下单:削峰与订单落库

503

10 年全栈工程师 · 503咖啡馆主理人

#热点库存#库存分片#热Key#售罄快速失败#分桶

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 4 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞