文章配乐
点击播放,边听边读。
秒杀场景,目标不是“写得优雅”,而是:高并发不超卖、DB 不被打穿、可降级、可恢复。你贴的那段在秒杀里会被瞬间打爆:读写分离、无原子、无事务、无幂等、无限流。
秒杀最常见的死法(你那段会触发的)
- 超卖:
SELECT+UPDATE分离(TOCTOU)。 - DB 打穿:每个请求都直连 MySQL 查库存。
- 热点行锁/死锁:大量请求更新同一行库存,锁竞争极高。
- 重复下单:重试/连点导致多次扣库存、插多单。
- 缓存击穿:Redis miss 或过期瞬间洪峰打到 DB。
- 服务不一致:扣库存成功但订单未落库、或落库失败没补偿。
秒杀推荐架构:Redis 预扣 + Lua 原子校验 + 异步落库 + MySQL 最终一致
关键思想:把“能不能买到”的判定放在 Redis(内存)做原子裁决;MySQL 只做最终落库。
请求链路(高并发版)
-
网关/应用层限流(令牌桶/漏桶,按活动/商品维度)
-
Redis Lua:原子判断 + 预扣库存 + 去重
- 库存不足:直接失败(不进 DB)
- 已买过:直接失败/返回已成功(幂等)
- 成功:写入消息队列/Stream(异步)
-
异步消费者:从队列取消息 → MySQL 事务落库 → 标记成功
-
客户端轮询/回调:查询下单结果(秒杀常见 202 Accepted)
1) Redis Lua 原子脚本(秒杀核心)
要实现三个能力:
- 库存 > 0 才扣
- 同一用户/幂等键只能成功一次
- 把成功请求写入队列(或 Stream)
示例(Redis Lua,伪 keys/args):
KEYS[1] = stockKey例如seckill:stock:productIdKEYS[2] = userSetKey例如seckill:users:productIdKEYS[3] = streamKey例如seckill:stream:productIdARGV = userId, idemKey, now
逻辑:
SISMEMBER userSet userId已成功过直接返回GET stock<=0 返回售罄DECR stockSADD userSet userIdXADD stream ...写入消息
返回:0售罄 / 2重复 / 1成功
这一步是秒杀“后端思维”的分水岭:一次原子脚本完成校验 + 扣减 + 入队,避免并发缝隙。
2) MySQL 落库策略(最终一致 + 不超卖兜底)
消费者侧(单线程/多分区)落库时:
- 用 幂等唯一键 防重复:
(product_id, user_id)或idem_key唯一 - 用事务插入订单 + 写明细(必要时)
- 库存同步到 MySQL 有两种策略:
策略 A(常用):MySQL 只做订单,不在高峰实时扣 MySQL 库存
- 秒杀库存以 Redis 为准
- 活动结束后批量把 Redis 扣减结果落到 MySQL(或写库存流水汇总)
- 适合:活动库存是独立池(秒杀专用库存)
策略 B(双写兜底):落库时也扣 MySQL 库存(更安全,吞吐更低)
UPDATE products
SET count = count - 1
WHERE id = ? AND count > 0;
如果 affectedRows=0:说明 Redis 与 MySQL 不一致(极少见,说明初始化/回滚有问题),这笔订单要标记异常并补偿(通常回滚用户资格/补偿库存)。
秒杀一般用 A(MySQL 不承受热点扣减),MySQL 作为最终事实账本。
3) 必备:幂等与唯一约束(秒杀不做这个=必翻车)
订单表:
CREATE TABLE seckill_orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
idem_key VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_pu (product_id, user_id),
UNIQUE KEY uk_idem (idem_key),
KEY idx_product (product_id),
KEY idx_user (user_id)
) ENGINE=InnoDB;
Redis 侧去重(userSet)是“快挡”,MySQL 唯一键是“最终挡”。
4) 回滚与补偿(你必须设计)
秒杀里最容易忽略:Redis 预扣成功了,但消费者落库失败怎么办?
做法:
-
消费失败重试(限定次数)
-
超过次数进入 DLQ(死信队列),人工/定时补偿
-
补偿动作通常是:
INCR stockSREM userSet userId- 标记订单失败(或根本没插入)
注意:补偿必须也是幂等的(重复执行不出错)。
5) 返回协议:不要同步返回“下单成功”
秒杀不要“接口里等 DB 插入完成”,否则把自己拖死。
建议:
-
Lua 成功后:返回
202 Accepted+requestId/idemKey -
提供
/seckill/result?idemKey=...查询结果:PENDING(排队中)SUCCESS(返回 orderId)FAIL_SOLD_OUT / FAIL_DUPLICATE / FAIL_SYSTEM
6) 最简“能上线版”(如果你现在就要改那段代码)
即使不引入 MQ,至少做到 MySQL 层原子扣减 + 事务 + 幂等(抗压一般,但不至于超卖):
UPDATE ... WHERE count > 0- 事务内插订单
- 唯一键兜底重复
- 限流 + 排队(哪怕是内存队列)减压
但真正秒杀,高峰还是建议 Redis Lua + 异步。
评论区
共 0 条评论,0 条主评论;回复统一显示为二级。