分库分表中单个库存Key成热点击穿源头,是因为业务层数据已水平拆分,但缓存层未同步逻辑分片,导致“数据分散、访问集中”;所有分片的扣减请求涌向同一Redis Key(如stock:sku1001),引发单节点过载与缓存失效后DB击穿。

分库分表中为什么单个库存 Key 会成热点击穿源头
分库分表本身不解决 Redis 热点问题,反而容易放大它。比如你把订单按 user_id 分了 1024 个库,但秒杀商品的库存还是统一存在一个 stock:sku1001 上——所有分片的扣减请求都涌向这一个 key,QPS 瞬间破万,Redis 单节点 CPU 扛不住,缓存一失效,全部请求直打数据库。
关键矛盾在于:业务层做了水平拆分,但缓存层没跟着做逻辑分片,导致“数据分散、访问集中”。这时候用普通分布式锁(如 SET stock:sku1001_lock "1" NX EX 10)只会让所有线程在同一个 key 上抢锁,锁竞争本身就成新瓶颈。
用分段锁替代全局锁:按 shard_id 拆锁粒度
核心是把“对一个 key 的锁”变成“对一组子 key 的锁”,让并发请求分散到不同锁上,避免锁排队阻塞。
- 先定义分段规则:比如用
sku_id % 16得到shard_id,生成分段锁 key:stock:sku1001:lock:0、stock:sku1001:lock:1…stock:sku1001:lock:15 - 扣减前,客户端计算当前请求所属分段:
shard_id = hash(user_id) % 16(注意:不能用sku_id,否则同商品所有请求仍落在同一段) - 只对对应分段锁尝试加锁:
SET stock:sku1001:lock:7 "req_abc123" NX EX 5,成功才继续扣减stock:sku1001:shard7 - 失败则短暂休眠后重试,或降级走本地限流(别死等)
这样原本 10 万 QPS 对一个锁的竞争,被摊到 16 个锁上,每个锁平均承受不到 7000 QPS,Redis 完全能扛住。
分段锁必须配合分段库存 Key,否则白搭
只分锁不分库存 key,等于给堵车路口多修了几条并行车道,但所有车还挤在同一条出口——依然卡死。
- 库存 key 必须也按同样规则分片:
stock:sku1001:shard0~stock:sku1001:shard15,每个分片初始值均分总库存(如总 16000,每片 1000) - 扣减时严格使用对应分片:
DECR stock:sku1001:shard7,不能跨片读写 - 查剩余库存需聚合所有分片:
redis.mget("stock:sku1001:shard0", ..., "stock:sku1001:shard15"),但这个操作频次低,可接受 - 注意:分片数不宜过多(>64),否则聚合成本高;也不宜过少(
Lua 脚本保证分段扣减的原子性与防超卖
光靠客户端分段 + 分段锁还不够。网络延迟、时钟漂移、锁过期重入,都可能导致同一分片被多次扣减。必须把“判断库存是否充足 + 扣减”封装进 Lua 脚本执行。
示例脚本(传入 KEYS[1] 为分片库存 key,ARGV[1] 为扣减数量):
if redis.call("GET", KEYS[1]) >= ARGV[1] then
return redis.call("DECRBY", KEYS[1], ARGV[1])
else
return -1
end
调用方式:redis.eval(script, 1, "stock:sku1001:shard7", "1")。这样即使锁已释放、其他请求进入,脚本内检查和扣减仍是原子的,彻底杜绝超卖。
容易忽略的一点:分段锁的 EX 时间必须大于 Lua 脚本执行耗时 + 网络往返,否则锁提前释放,脚本还没跑完就被其他请求抢占——建议设为 5~10 秒,并监控实际执行时长。











