Redis实践篇(秒杀活动)

322 阅读8分钟

秒杀是一个非常典型的活动场景,比如,在双 11、618 等电商促销活动中,都会有秒杀场景。秒杀场景的业务特点是限时限量,业务系统要处理瞬时的大量高并发请求,而 Redis就经常被用来支撑秒杀活动。

不过,秒杀场景包含了多个环节,可以分成秒杀前、秒杀中和秒杀后三个阶段,每个阶段的请求处理需求并不相同,Redis 并不能支撑秒杀场景的每一个环节。

秒杀的特征

  1. 瞬时流量高:一般数据库每秒只能支撑千级别的并发请求,而 Redis 的并发处理能力(每秒处理请求数)能达到万级别,甚至更高。所以,当有大量并发请求涌入秒杀系统时,我们就需要使用 Redis 先拦截大部分请求,避免大量请求直接发送给数据库,把数据库压垮。

  2. 读多写少:在秒杀场景下,用户需要先查验商品是否还有库存(也就是根据商品 ID 查询该商品的库存 还有多少),只有库存有余量时,秒杀系统才能进行库存扣减和下单操作。

秒杀前

在这个阶段,用户会不断刷新商品详情页,这会导致详情页的瞬时请求量剧增。这个阶段的应对方案,一般是尽量把商品详情页的页面元素静态化,然后使用 CDN 或是浏览器把这些静态化的元素缓存起来。这样一来,秒杀前的大量请求可以直接由 CDN 或是浏览器缓存服务,不会到达服务器端了,这就减轻了服务器端的压力。

秒杀中

此时,大量用户点击商品详情页上的秒杀按钮,会产生大量的并发请求查询库存。一旦某个请求查询到有库存,紧接着系统就会进行库存扣减。然后,系统会生成实际订单,并进行后续处理,例如订单支付和物流服务。如果请求查不到库存,就会返回。用户通常会继续点击秒杀按钮,继续查询库存。 简单来说,这个阶段的操作就是三个:库存查验、库存扣减和订单处理。

  1. 秒杀按钮点击后置灰,只有当服务端返回响应后才能再次点击;
  2. 请求拦截和流控,对于请求明显不正常,或者单个ip请求数过大,则进行拦截;
  3. 库存查询使用Redis支撑,防止请求到数据库;
  4. 商品库存提前预热到redis, 同时评估流量过大的话,可以将库存分段,分区存储到不同的redis实例用于分担秒杀的请求流量;
  5. 库存查询后,进行扣减时,扣减操作不能放在数据库,因为一旦放到数据库,就需要跟redis进行库存同步,否则发生商品超卖。而且,为了避免请求查询到旧的库存值,库存查验和库存扣减这两个操作需要保证原子性。这里可用lua脚本来实现。
  6. 当库存查询和库存扣减的原子性操作成功后,发送到消息队列异步生成订单,提升吞吐量,此时才真正的去扣减数据库的商品库存。同时, 如果需要严格限制超卖,这里也可以进一步的做兜底校验,加锁,判断商品库存是否足够,不足够则作为异常订单处理。
  7. 秒杀商品的库存信息用单独的实例保存,而不要和日常业务系统的数据保存在同一个实例上,这样可以 避免干扰业务系统的正常运行。

image.png

秒杀场景对 Redis 操作的根本要求有两个

  1. 支撑高并发
  2. 库存查询和库存扣减的原子性保障

秒杀的库存查询和库存扣减的lua脚本:

# 获取商品库存信息
local counts = redis.call("HMGET", KEYS[1], "total", "ordered"); 
# 将总库存转换为数值
local total = tonumber(counts[1]) 
# 将已被秒杀的库存转换为数值
local ordered = tonumber(counts[2]) 
#如果当前请求的库存量加上已被秒杀的库存量仍然小于总库存量,就可以更新库存
if ordered + k <= total then 
    # 更新已秒杀的库存量
    redis.call("HINCRBY",KEYS[1],"ordered", k) 
    return k 
end 
    return 0

问题:假设一个商品的库存量是 800,我们使用一个包含了 4 个实例的切片集群来服务秒杀请求。我们让每个实例各自维护库存量 200,然后,客户端的秒杀请求可以分发到不同的实例上进行处理,你觉得这是一个好方法吗?

优点:

  1. 各实例独立处理请求,流量被分散,较少了redis的压力,无需分布式锁或跨节点通信,减少了延迟和复杂度;
  2. 单个实例故障仅影响其分配的库存(如200),不会导致全局雪崩。

缺点:

  1. 库存分配不均可能导致部分实例库存未售完,而其他实例已售罄,总体库存利用率低。
  2. 如果某个实例故障,其对应的库存无法销售,影响总库存。
  3. 请求分布不均可能导致某些实例过载或库存提前耗尽,影响用户体验。
  4. 无法动态调整库存分配,导致灵活性差。

适用场景:对性能要求极高、允许少量库存损耗的场景(如电商引流活动)。

秒杀活动的技术挑战总结

关键技术点总结

挑战解决方案
高并发缓存预扣减、请求排队、异步化处理
防超卖原子操作(Redis/Database)、分布式锁、库存分片
数据一致性最终一致性(消息队列+补偿任务)
系统可用性限流熔断、服务降级、多级缓存

1. 前端优化

目标:拦截无效流量,降低后端压力

  • 静态资源缓存:将商品详情页静态化(HTML+CDN),减少动态请求。
  • 请求频率限制:客户端(如JavaScript)限制用户频繁点击(如点击后按钮置灰)。
  • 验证码/答题:在秒杀开始前加入验证码或简单数学题,过滤机器人请求。
  • 请求排队:通过进度条或队列机制,将用户请求分批放行到后端。

2. 网关层

目标:统一入口,拦截恶意请求

  • 限流熔断

    • 使用令牌桶、漏桶算法限制每秒请求量(如Nginx limit_req)。
    • 对异常IP或用户实施黑名单拦截。
  • 负载均衡

    • 动态路由:根据后端服务实例的负载或库存状态分发请求(如库存不足的实例不再接收新请求)。
    • 一致性哈希:确保同一用户的请求落到固定实例,减少缓存穿透。

3. 服务层

目标:高效处理秒杀逻辑,保障库存一致性

  • 库存预扣减

    • 方案一(内存操作+异步落库)

      1. 使用Redis集群存储库存(如DECR原子操作扣减)。
      2. 扣减成功后生成订单号,写入消息队列(如Kafka)。
      3. 异步消费队列,持久化订单到数据库。
        优点:高性能;缺点:需处理Redis与DB的数据一致性。
    • 方案二(数据库乐观锁)

      1. 通过数据库行级锁或版本号实现扣减(如UPDATE stock SET count=count-1 WHERE id=xxx AND count>0)。
      2. 扣减成功后同步创建订单。
        优点:强一致;缺点:数据库压力大,需配合缓存使用。
  • 防重复请求

    • 用户ID+商品ID生成唯一请求ID,Redis记录已处理请求(SETNX),只允许购买一次。

4. 数据层

目标:支撑高并发读写,避免成为瓶颈

  • 缓存抗压

    • Redis集群缓存库存、商品详情、用户令牌等热点数据。
    • 使用Lua脚本保证库存操作的原子性(如DECR和订单生成的组合操作)。
  • 数据库优化

    • 分库分表:按商品ID或用户ID分片,分散写入压力。
    • 批量写入:消息队列异步消费后批量落库,减少事务开销。
    • 最终一致性:通过补偿任务(如定时对账)处理Redis与DB的数据差异。

5. 容灾与降级

目标:保障系统可用性,防止雪崩

  • 服务降级

    • 当库存接近耗尽时,直接返回“已售罄”页面,减少后端调用。
    • 关闭非核心功能(如用户积分扣除)。
  • 熔断机制

    • 使用Hystrix或Sentinel监控服务状态,异常时快速熔断。
  • 库存预热

    • 提前将库存加载到Redis,避免瞬时数据库查询压力。