设计秒杀系统
问题
如何用 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 分钟未支付则取消订单并回补库存。