跳到主要内容

设计秒杀系统

问题

如何用 Go 设计一个高并发秒杀系统?

答案

核心架构

关键组件

1. Redis 库存预扣(原子操作)

// Lua 脚本保证原子性
const decrStockLua = `
local stock = redis.call('GET', KEYS[1])
if not stock then return -1 end
if tonumber(stock) <= 0 then return 0 end
return redis.call('DECR', KEYS[1])
`

func TryDecrStock(ctx context.Context, rdb *redis.Client, skuID string) (bool, error) {
result, err := rdb.Eval(ctx, decrStockLua, []string{"stock:" + skuID}).Int64()
if err != nil {
return false, err
}
return result > 0, nil // >0 表示扣减成功
}

2. 请求排队(Channel 缓冲)

type SeckillRequest struct {
UserID string
SkuID string
Result chan<- bool
}

// 缓冲队列,超出直接拒绝
var queue = make(chan SeckillRequest, 10000)

func HandleSeckill(userID, skuID string) bool {
ch := make(chan bool, 1)
select {
case queue <- SeckillRequest{UserID: userID, SkuID: skuID, Result: ch}:
// 等待处理结果
return <-ch
default:
// 队列满,直接拒绝
return false
}
}

// 消费者 goroutine
func ConsumeOrders(rdb *redis.Client, db *gorm.DB) {
for req := range queue {
ok, _ := TryDecrStock(context.Background(), rdb, req.SkuID)
if ok {
// 异步创建订单
go createOrder(db, req.UserID, req.SkuID)
}
req.Result <- ok
}
}

3. 防重复提交

func PreventDuplicate(rdb *redis.Client, userID, skuID string) bool {
key := fmt.Sprintf("seckill:%s:%s", skuID, userID)
// SETNX,设置过期时间
ok, _ := rdb.SetNX(context.Background(), key, 1, 10*time.Minute).Result()
return ok // true = 首次提交
}

多层防护

层级措施作用
前端按钮置灰、倒计时减少无效请求
CDN页面静态化降低服务端压力
网关IP 限流、令牌桶拦截恶意请求
应用Redis 预扣 + 队列保护数据库
数据库乐观锁扣减最终一致性
核心思路

秒杀系统的本质是将并发写变为串行写,通过 Redis 预扣减库存 + 异步创建订单,避免数据库被高并发打垮。


常见面试问题

Q1: 如何防止超卖?

答案

  • Redis Lua 脚本原子扣减(主防线)
  • 数据库乐观锁兜底:UPDATE stock SET count=count-1 WHERE sku_id=? AND count>0
  • 两层保护确保不超卖

Q2: Redis 挂了怎么办?

答案

  • Redis Sentinel/Cluster 保证高可用
  • 降级方案:直接走数据库乐观锁(QPS 会降低,需配合限流)
  • 提前预热:活动开始前将库存加载到 Redis

Q3: 如何处理未支付订单释放库存?

答案:用延迟队列(Redis ZSet 或消息队列延迟消息),15 分钟未支付则取消订单并回补库存。

相关链接