单 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 异步下单:削峰与订单落库。
评论 (0)