云计算百科
云计算领域专业知识百科平台

双 11 秒杀库存预热方案:将 MySQL 真实库存提前同步至 Redis 集群的防超卖实践

在大促秒杀系统里,最让后端工程师夜不能寐的两个词,一个是“超卖”(Over-selling),另一个是“击穿”(Cache Breakdown)。

超卖意味着实际库存只有 100 件,系统却由于并发脏读扣减了 150 件,最终不得不向用户打电话道歉赔偿,带来直接的资金与声誉损失;而击穿则意味着秒杀开抢的第一毫秒,上万个并发请求直接绕过缓存打穿到 MySQL,引发全表行级锁争用,数据库瞬间锁死宕机。

很多新手常犯的错误,就是以为在应用层写个分布式锁(如 Redisson 或原生 SETNX),或者在 SQL 里加一句 WHERE stock >= quantity 就完事了。在几千上万 QPS 的秒杀洪峰面前,只要请求真正碰到了 MySQL 的写盘操作,数据库的吞吐天花板(通常只有一两千 TPS)就会立刻成为全站的断头路。

核心原则只有一条:秒杀库存扣减必须 100% 在 Redis 内存中完成,绝不允许一个未扣减成功的写请求打到数据库。


一、库存预热与对齐的标准执行流程

要让 Redis 成为一道坚不可摧的防火墙,关键在于大促开抢前 10 分钟的库存预热与状态对齐:

[ 秒杀开抢前 15 分钟 (T-15m) ]
│
▼ (定时预热任务)
[ 从 MySQL 锁定待秒杀商品库存 ]
├── 状态标记为 "预热中" (防止后台继续被修改)
└── 生成全局唯一的商品秒杀版本号 (VersionID)
│
▼ (写入 Redis 集群)
[ 灌入 Redis 内存 Key ]
├── string: seckill:stock:sku_1001 -> "100" (真实库存)
├── hash: seckill:users:sku_1001 -> {} (防重防刷排重表)
└── string: seckill:soldout:sku_1001 -> "0" (售罄熔断标记)
│
▼ (T-1m 预热完毕)
[ 预热校验核对无误,开放前端倒计时 ]


二、Lua 脚本实现原子扣减与防超卖双保险

在 Redis 中扣减库存,绝对不能分步执行 GET stock 然后判断再 DECR。网络往返存在天然的时间窗口,多并发下必发超卖。必须使用一段严谨的原子 Lua 脚本,在一个事务内完成防刷校验与库存扣减:

— KEYS[1]: 库存键名 (如 seckill:stock:sku_1001)
— KEYS[2]: 防刷排重键名 (如 seckill:buyers:sku_1001)
— ARGV[1]: 当前下单用户唯一 UID
— ARGV[2]: 扣减数量 (例如 1)

local stock_key = KEYS[1]
local buyers_key = KEYS[2]
local uid = ARGV[1]
local quantity = tonumber(ARGV[2])

— 1. 幂等校验:同一用户在同一次秒杀中仅限抢购一次
if redis.call('HEXISTS', buyers_key, uid) == 1 then
return -1 — 重复抢购,直接拦截
end

— 2. 读取当前剩余库存
local current_stock = tonumber(redis.call('GET', stock_key) or "0")

— 3. 判断库存是否充足
if current_stock < quantity then
return 0 — 库存不足,已售罄
end

— 4. 原子扣减库存并记录成功买家
redis.call('DECRBY', stock_key, quantity)
redis.call('HSET', buyers_key, uid, 1)

return 1 — 扣减成功,发放购买凭据


三、Go 语言实现的高并发秒杀拦截器

在 Go 业务端,我们在内存中维护了一套极轻量的单机本地售罄标记(In-Memory Soldout Flag)。一旦某个商品在 Redis 中返回了库存为 0,该标记在本地进程内立刻置为 true。后续涌进来的数万并发请求,直接在本地内存返回“已售罄”,连一次 Redis 网络 I/O 都省掉了。

package seckill

import (
"context"
"errors"
"fmt"
"sync"
)

type SeckillManager struct {
mu sync.RWMutex
soldoutFlags map[int64]bool // 本地售罄快速熔断缓存
redisClient RedisLuaExecutor
}

type RedisLuaExecutor interface {
ExecSeckillLua(ctx context.Context, skuID int64, userID string, quantity int) (int, error)
}

func NewSeckillManager(client RedisLuaExecutor) *SeckillManager {
return &SeckillManager{
soldoutFlags: make(map[int64]bool),
redisClient: client,
}
}

// TryAcquireSeckillQuota 秒杀配额获取入口
func (m *SeckillManager) TryAcquireSeckillQuota(ctx context.Context, skuID int64, userID string, quantity int) error {
// 第一道检查:单机内存直接阻断(0 网络开销)
m.mu.RLock()
if m.soldoutFlags[skuID] {
m.mu.RUnlock()
return errors.New("商品已抢光 (本地快速熔断)")
}
m.mu.RUnlock()

// 第二道检查:打入 Redis 执行原子 Lua 脚本
status, err := m.redisClient.ExecSeckillLua(ctx, skuID, userID, quantity)
if err != nil {
return fmt.Errorf("redis execution error: %w", err)
}

switch status {
case 1:
// 扣减成功,可以异步投递至 Kafka,由后台平滑入库落盘
return nil
case 0:
// 已经售罄:立刻更新本地标记,阻断后续并发打扰 Redis
m.mu.Lock()
m.soldoutFlags[skuID] = true
m.mu.Unlock()
return errors.New("手慢了,商品已抢光")
case -1:
return errors.New("您已参与过本次秒杀,请勿重复下单")
default:
return errors.New("系统繁忙,请稍候再试")
}
}


四、异步回写数据库与最终一致性避坑

在 Redis 扣减成功后,真正的“持久化订单创建”绝对不能同步去写 MySQL,而是采用可靠消息队列(如 Kafka / RabbitMQ)进行削峰异步回写:

  • 消息队列平滑消费:将并发 10,000 QPS 的瞬时爆发请求压入 Kafka,后台由 8 个 Worker 线程以每秒 500 次的平稳速率向 MySQL 执行批处理插入(INSERT INTO orders …),主库毫无波澜。
  • 库存回滚补偿(Saga 机制):如果用户抢到资格后在 15 分钟内未完成最终支付,超时未支付的定时扫描任务必须通过逆向 Lua 脚本:INCRBY seckill:stock:sku_1001 1 并删除 buyers_key 里的 UID,让库存平滑回归,绝不压死任何一张有效库存。

  • 五、架构师高并发防超卖 Checklist

    在将秒杀扣库存链路推向生产大促前,必须逐项核对以下架构防线:

  • 唯一业务约束强兜底(Database Hard Constraint):在持久化订单表中,必须为 sku_id + user_id 建立联合唯一索引(uk_sku_user)。哪怕消息队列发生网络重试甚至重复消费,数据库底层的物理唯一索引也能在毫秒级彻底拦截重复落单,坚决不给任何并发漏洞留下空间。
  • 热点预热与缓存击穿防护:大促开抢前 30 分钟,后台运维系统必须对参与秒杀的全部 SKU 完成缓存预热,并在 Redis 键过期时间上增加 5~10 分钟的随机偏移量(Jitter),杜绝成百上千个热点键在同一秒内集体失效导致缓存雪崩。
  • 异步对账与孤儿库存自愈:设置专门的后台巡检对账 Worker,每隔 5 分钟比对一次 Redis 剩余库存与 MySQL 真实落盘订单总数。一旦发现由于节点意外断网导致的差异,自动生成补偿事件触发平滑校准,确保库存账实 100% 相符。
  • 通过这套“提前预热 + 内存阻断 + 原子 Lua + 异步削峰 + 数据库硬约束”的组合拳,我们在双 11 前的三轮全链路压测中实现了秒杀接口 12,000 QPS 零故障、零超卖、零数据库阻塞,真正把高并发系统的防线铸成了铜墙铁壁。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 双 11 秒杀库存预热方案:将 MySQL 真实库存提前同步至 Redis 集群的防超卖实践
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!