Redis - 支撑秒杀场景的关键技术与实践


在这里插入图片描述

秒杀是互联网业务里压力最大的场景之一:百万用户在毫秒级时间窗口内涌入,争抢有限的库存。任何一个环节扛不住,要么超卖、要么宕机。Redis 在这里几乎是标配,但要把它用对,需要理解秒杀的本质瓶颈和 Redis 在每个环节的角色。

秒杀的特点与挑战

秒杀和普通业务完全不同:

  • 流量瞬时极高:平时 QPS 几千的系统,秒杀瞬间可能冲到几十万。
  • 请求高度集中:所有请求都打在同一个商品的库存 key 上。
  • 有效请求很少:库存只有 1000,但可能有 100 万人来抢,99% 的请求是注定失败的。
  • 不能超卖:无论并发多高,卖出的商品数不能超过库存。
  • 公平性要求:先到先得,不能让后来的请求抢先。

秒杀的几个阶段

完整的秒杀流程通常分为四个阶段:

1. 秒杀前

用户访问商品详情页,刷新等待开抢。这个阶段流量大但都是只读请求。

Redis 角色:缓存商品详情、活动信息。所有静态数据走 CDN 和本地缓存,Redis 只兜底。

2. 秒杀进行中

开抢瞬间,前面三件事密集发生:

  • 用户提交订单请求
  • 校验是否有资格(白名单、限购)
  • 扣减库存

Redis 角色:库存计数器、用户去重、限流。

3. 下单付款

抢到的用户进入下单页面,最终完成支付。这个阶段的并发量已经远小于第二阶段(只有库存数量的用户)。

Redis 角色:临时订单存储、支付状态。

4. 秒杀后

发货、售后等。和普通业务无异。

性能最关键的就是第二阶段。

Redis 在秒杀中的核心作用

库存预扣减

最核心的诉求:保证不超卖

错误做法:在数据库做扣减。MySQL 的行锁会让所有请求排队,瞬间打爆数据库。

正确做法:把库存放在 Redis,用原子命令扣减。

# 初始化
SET stock:item_1001 1000

# 扣减
DECR stock:item_1001
# 返回值 >= 0 表示扣减成功
# 返回值 < 0 表示已经卖完

DECR 是原子操作,一万个客户端同时执行也不会超卖。Redis 的吞吐能轻松支撑 10 万 QPS。

但这种简单方案有个问题:返回值 < 0 时还是扣减了一次,需要再补回去:

DECR stock:item_1001
# 如果 < 0,做 INCR 补回
INCR stock:item_1001

中间这个补偿操作不是原子的,并发下可能让某些 key 短暂为负。

Lua 脚本:扣减 + 去重

更稳妥的方案是用 Lua 脚本,把"判断库存 + 扣减 + 记录用户"打包成原子操作:

local stock = tonumber(redis.call('GET', KEYS[1]))
if stock <= 0 then
    return 0
end
local exists = redis.call('SISMEMBER', KEYS[2], ARGV[1])
if exists == 1 then
    return -1   -- 已经买过
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1

调用:

EVAL "..." 2 stock:item_1001 buyers:item_1001 user_8888

返回值约定:

  • 1:抢购成功
  • 0:库存已空
  • -1:用户已经抢过(防重复)

一次脚本调用搞定多个判断,避免来回网络。

用户限购

不同业务限购规则不同,常见的:

  • 每人只能抢 1 件:Set 记录已购用户。
  • 每人最多 N 件:Hash 记录每个用户的购买数量。
  • 特定用户群体可抢:白名单 Set。

这些都是 Redis 的强项。

切片集群下的秒杀

单实例 Redis 能扛 10 万级 QPS,但更大的活动可能瞬间百万 QPS。这时需要切片集群。

但秒杀场景下切片集群有个棘手问题:所有请求都打到同一个 key,路由到同一个节点,集群的负载完全不均衡——一个节点忙死,其他节点闲着。

应对思路:库存分桶。把商品的库存拆成多个 key 分散到不同节点:

原方案:stock:item_1001 = 1000
新方案:stock:item_1001:0 = 100
        stock:item_1001:1 = 100
        ...
        stock:item_1001:9 = 100

请求来了,按用户 ID hash 选一个桶:

bucket = user_id % 10
key = f"stock:item_1001:{bucket}"

这样能把负载分散到 10 个节点。代价是逻辑变复杂:

  • 某个桶卖完了,那部分用户就抢不到,即使其他桶还有库存。
  • 需要更精细的桶分配策略。

实际工程上,桶数和总库存的关系要权衡。库存 1000 拆 10 个桶还行,但库存 100 拆 10 个桶就变成"每桶 10 件",公平性受影响。

异步下单:削峰填谷

秒杀第二阶段的目标是快速判断有没有抢到,至于真正的订单生成、扣减真实库存(数据库)、支付链路启动,都可以异步进行。

典型架构:

用户请求
   ↓
Redis 扣库存(原子)
   ↓
抢到的用户 → 投递到消息队列
   ↓
后端慢慢消费 → 创建订单 → 扣 DB 库存 → 发短信

Redis 在前端挡住瞬时高并发,消息队列在中间削峰,后端按自己的速度消费。这是秒杀架构的经典模式。

防止恶意刷单

秒杀场景下黑产极活跃,常见手段:

  • 同一 IP / 用户 ID 高频请求。
  • 用脚本提前请求接口。
  • 多账号协同。

Redis 在限流层面可以做:

# IP 限流:1秒内不超过 10 次
INCR rate_limit:1.2.3.4
EXPIRE rate_limit:1.2.3.4 1
# 如果 > 10 直接拒绝

业务层还要配合:

  • 验证码(图形 / 滑块)。
  • 接口签名校验。
  • 用户风控等级。

实践建议

  1. 核心扣减必须用 Lua,简单 DECR 容易出现库存为负的瞬态。
  2. 库存分桶应对超大流量,但要权衡公平性。
  3. 削峰必须有消息队列,Redis 不是万能的。
  4. 预热缓存:开抢前把商品详情、用户信息预加载到 Redis。
  5. 限流前置:在 Nginx 或网关层就开始限流,别让所有请求都打到 Redis。
  6. 降级方案:极端情况 Redis 也可能挂,备一套静态降级页面。
  7. 监控关键指标:库存 key 的 QPS、Lua 脚本耗时、抢到与未抢到的比例。

秒杀不是一个单点能解决的问题,是从前端、网关、应用、缓存、队列、数据库的全链路设计。Redis 在中间是关键一环,但不是全部。理解 Redis 的能力边界(高吞吐、低延迟、原子操作),把它用在最合适的位置——库存预扣、用户去重、限流计数——就能撑起绝大多数秒杀场景。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值