共计 1995 个字符,预计需要花费 5 分钟才能阅读完成。
背景痛点:秒杀系统的经典难题
电商大促期间的龙虾必装 skill 服务,本质上是典型的秒杀场景。这类业务往往面临几个核心挑战:

- 库存超卖问题:当多个请求同时扣减库存时,若没有原子性保证,会导致实际销量超过库存。例如 100 件商品可能卖出 120 件。
- 接口响应暴增:瞬时 QPS 可能从平时 100 激增至 10 万 +,引发服务雪崩。
- 数据库压力:直接读写数据库会导致连接池耗尽,MySQL 的 TPS 上限成为瓶颈。
我们曾监测到某次大促中,库存校验接口平均响应时间 (ART) 从 50ms 飙升至 2.3 秒,超时率高达 35%。
技术选型:为什么是 Go+Redis?
对比常见解决方案:
- 分布式锁方案:
- 优点:实现简单(如 Redlock)
-
缺点:锁竞争成为新瓶颈,TTL 设置需要权衡
-
消息队列方案:
- 优点:天然削峰能力
-
缺点:强依赖 MQ 可靠性,增加系统复杂度
-
Redis+Lua 方案:
- 原子性:单个 Lua 脚本在 Redis 中天然原子执行
- 性能:内存操作 + 单线程模型适合计数场景
- 扩展性:可通过集群分片横向扩容
最终选择 Go+Redis 组合,因为:
– Go 的 goroutine 轻量级线程模型适合高并发
– Redis 的 INCR/DECR 指令性能可达 10W+ OPS
– 两者组合部署成本低于引入 MQ 全家桶
核心实现
1. 库存扣减的 Lua 脚本
-- KEYS[1]: 库存 key ARGV[1]: 扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
else
return -1
end
Go 中调用示例:
script := redis.NewScript(`... 上面 lua 代码...`)
result, err := script.Run(ctx, rdb, []string{"stock:123"}, 1).Int()
2. 异步削峰队列
使用 Redis ZSET 实现请求排队:
// 入队(毫秒时间戳作为 score)zaddArgs := redis.ZAddArgs{
NX: true,
Members: []redis.Z{{Score: float64(time.Now().UnixMilli()), Member: requestID},
},
}
rdb.ZAddArgs(ctx, "queue:skill", zaddArgs)
// 消费者轮询
for {
items := rdb.ZRangeByScore(ctx, "queue:skill", &redis.ZRangeBy{
Min: "0",
Max: strconv.FormatInt(time.Now().UnixMilli(), 10),
}).Val()
// 处理 items 并 ZREM 删除已处理项
}
3. 热点数据预热
启动时加载策略:
func preloadHotItems() {items := getTop100ItemsFromDB() // 伪代码
for _, item := range items {err := rdb.Set(ctx, "hot:"+item.ID, item.Stock, 24*time.Hour).Err()
// ... 错误处理
}
}
性能验证
压测环境配置:
– 8 核 16G 服务器 ×3
– Redis 集群 6 节点
| 场景 | QPS | P99 延迟 | 超时率 |
|---|---|---|---|
| 直接查 DB | 1,200 | 1.8s | 42% |
| 优化后方案 | 38,000 | 68ms | 0.3% |
避坑指南
- Redis 集群的 Lua 陷阱:
- 所有 key 必须位于相同 slot(可用 hash tag 确保)
-
避免在 Lua 中执行耗时的 for 循环
-
连接池打满对策:
rdb := redis.NewClient(&redis.Options{ PoolSize: 1000, // 根据压测调整 MinIdleConns: 50, // 保持常驻连接 }) -
突发流量缓冲:
- 使用令牌桶算法限流
- 返回 ” 稍后再试 ” 比直接拒绝更友好
扩展思考:预售功能设计
在现有架构上扩展预售:
1. 新增预售库存字段 pre_stock
2. Lua 脚本中增加预售逻辑分支
3. 订单支付后异步同步到 DB
4. 用单独的 Redis Stream 处理超时未支付订单
-- 伪代码:含预售的库存判断
if normal_stock > 0 then
deduct_normal_stock()
elseif pre_stock > 0 then
deduct_pre_stock()
set_order_pre_flag()
end
最终效果:
– 原有秒杀逻辑不受影响
– 预售订单有单独处理链路
– 仍保持库存操作的原子性
总结
这套方案在某生鲜平台 618 大促中验证,核心接口承受住了 12 万 QPS 的冲击。关键收获:
- 简单即美:避免过度设计,Redis 单机版能扛住大部分场景
- 监控先行:提前部署 Redis 慢查询监控和 Go 的 pprof
- 容灾预案:准备降级策略(如直接返回 ” 已售罄 ”)
代码已开源在 GitHub(伪代码需替换为实际项目地址),欢迎交流优化建议。
