
26. 实战案例复盘
——秒杀库存扣减 Lua 脚本,排行榜赛季结算 2 亿条数据,社交平台 Feed 流推送
一、秒杀库存扣减:把并发锁做成“单线程”
-
业务背景
大促峰值 18 w/s,SKU 总数 2 000,单 SKU 最大 6 w/s。早期用 Redis + 分布式锁,CPU 空转 35 %,超卖 12 件。 -
设计目标
0 超卖、1 ms 内返回、横向可扩展、降级方案不击穿 DB。 -
核心思路:把“锁”预埋在 Lua 里
(1)数据模型
sku:1001:stock // 剩余库存
sku:1001:order // 已购用户集合(去重)
sku:1001:lock // 分布式互斥令牌,过期 50 ms
(2)Lua 脚本(精简版)
local sku = KEYS[1]
local uid = ARGV[1]
local now = tonumber(ARGV[2])
-- 1. 重复购买检查
if redis.call('SISMEMBER', sku..':order', uid) == 1 then
return {-1, 'REPEAT'}
end
-- 2. 令牌锁,防多集群并发
if redis.call('EXISTS', sku..':lock') == 1 then
return {-2, 'LOCKED'}
end
-- 3. 库存扣减
local left = redis.call('DECR', sku..':stock')
if left < 0 then
redis.call('INCR', sku..':stock') -- 回滚
return {-3, 'SOLD_OUT'}
end
-- 4. 记录用户
redis.call('SADD', sku..':order', uid)
redis.call('EXPIRE', sku..':order', 86400)
-- 5. 下发延迟令牌,用于异步关单
redis.call('ZADD', 'sku:delay', now+600, sku..':'..uid)
return {left, 'SUCCESS'}
(3)性能
6 w/s 单 SKU 压测,P99 0.8 ms;CPU 降至 12 %;0 超卖。
-
踩坑记录
① EVALSHA 缓存穿透:预热脚本列表,脚本变更版本号 + 1。
② bigkey 阻塞:拆成 1 000 子桶,脚本内做二次 hash。
③ 节点宕机迁移:stock 与 order 落在同一 slot,保证原子。 -
降级链路
Lua 拒单 → 发送 MQ → 消费端二次校验 DB 库存 → 真超卖时人工补货。
二、排行榜赛季结算:2 亿条纪录 5 min 跑完
-
业务规模
赛季 30 天,日活 800 w,写入 9 000 w/天,共 2.1 亿条积分明细。 -
老方案痛点
MySQL 聚合 + 单线程排序,结算窗口 4 h,高峰 CPU 90 %,影响在线业务。 -
新架构:Redis 跳表 + 离线合并
(1)实时写
ZINCRBY rank:2025S1 3 user:123
写入同时落地 Kafka,用于离线对账。
(2)赛季末流程
Step-1 并行 Dump
Redis Cluster 16 个 master,各自执行 redis-cli --rdb,RDB 落 HDFS,5 min。
Step-2 MapReduce 去重合并
Key -> userId,Value -> score,相同 user 多分区累加,输出 <userId, totalScore>,2 亿 → 1.2 亿。
Step-3 Spark SQL 排序
df.sortBy($"score".desc).zipWithIndex 生成 rankId,持久化到 ClickHouse 本地表。
Step-4 写回 Redis 新 Key
ZADD rank:2025S1:final rankId userId,用于新赛季榜单展示。
-
性能
200 核 Spark 集群,5 min 完成排序;Redis 只读 1 Gbps,无阻塞。 -
一致性保障
① RDB 快照时间戳对齐,所有节点在同一 slave 上执行LASTSAVE确认。
② 离线结果与 Redis 实时榜差异 < 0.01 %,超出阈值触发人工复核。
三、社交平台 Feed 流推送:读扩散 + 写扩散混合
-
场景
关注模型 30 亿边,日新增 Feed 8 000 w,首页 Timeline 要求 P99 < 300 ms。 -
模型选型
大 V 写扩散,普通用户读扩散,阈值 1 w 粉丝。 -
写扩散(Push)
(1)表结构
feed:{userId}:{timestamp}-> protobuf 内容
inbox:{fanId}-> Redis List,LPUSH 新消息 ID,LTRIM 保持 500 条。
(2)并发写入
使用 Redis pipeline,单次 100 条,batch 大小 8 k,单机 40 w/s。
(3)冷启动补偿
粉丝 > 1 w 的大 V 发 Feed 时,若 fan 的 inbox 为空,触发异步任务补全最近 20 条,防止首刷空窗。
- 读扩散(Pull)
(1)小 V 不发 inbox,只写 feed:userId;粉丝读取时实时聚合:
ZUNIONSTORE tmp 5 feed:101 feed:102 ... WEIGHTS 1 1
ZREVRANGE tmp 0 20
(2)聚合结果缓存 5 s,缓存键 union:{fanId}:{slice},slice = 时间戳 / 5。
-
热点防护
① 本地缓存 + 异步合并:Guava 缓存 1 s,同一 fanId 请求合并为一次 ZUNION。
② 大 V 突然爆炸:粉丝列表拆 1 k 桶,写入限速 2 w/s,超量转读扩散。 -
性能
线上 22 w/s 写扩散,P99 写入延迟 18 ms;首页聚合 P99 220 ms,缓存命中率 96 %。
四、三板斧共性总结
- 把“并发原语”压进单线程:Lua、pipeline、单 Key 事务,消灭分布式锁。
- 让数据围着计算走:RDB dump + 离线 Spark,避免在线大排序。
- 读写模型动态切换:阈值、桶、降级开关,实时根据画像调整策略。
五、留给下一个赛季的 TODO
• 秒杀:探索 Redis 7 Function,把 Lua 升级为 JS,脚本热更新不下线。
• 排行榜:引入 Redis 7.4 的 JSON.TOPK,实时维护赛季 Top100,省去离线合并。
• Feed:研究 Dragonfly 的 LFRU 缓存,单节点 4 QPS 提升 40 %,减少分桶复杂度。
更多技术文章见公众号: 大城市小农民
3万+

被折叠的 条评论
为什么被折叠?



