分布式缓存:应对突发流量的缓存体系构建

发布于:2025-06-18 ⋅ 阅读:(21) ⋅ 点赞:(0)

在这里插入图片描述


缓存全景图

在这里插入图片描述


Pre

分布式缓存:缓存设计三大核心思想

分布式缓存:缓存的三种读写模式及分类

分布式缓存:缓存架构设计的“四步走”方法

分布式缓存:缓存设计中的 7 大经典问题_缓存失效、缓存穿透、缓存雪崩

分布式缓存:缓存设计中的 7 大经典问题_数据不一致与数据并发竞争

分布式缓存:缓存设计中的 7 大经典问题_Hot Key和Big Key

分布式缓存:三万字详解Redis

分布式缓存:ZSET → MGET 跨槽(cross‐slot)/ 并发 GET解决思路

分布式缓存:CAP 理论在实践中的误区与思考

分布式缓存:BASE理论实践指南


每日一博 - 闲聊“突发流量”的应对之道

背景与目标说明

短时间内流量剧增,后端需应对大量无效请求并快速响应成功请求。核心思路是:将绝大多数请求在上游拦截或由缓存处理,不让每个请求直击后端数据库和核心服务,同时保证库存操作原子、正确。缓存体系是关键环节,可极大提高查询速度、减轻数据库压力、提升用户体验。


缓存原则与设计思路

  • 上游过滤、预处理优先:在到达核心库存/下单逻辑前,用缓存快速判断活动状态、库存是否售罄、用户是否有资格、是否已下单等,避免无效请求流向下游。
  • 缓存优先、后端降级:常见数据(商品详情、秒杀配置信息)使用缓存读取;若缓存不可用或命中率低,再访问后端,并在适当时机补回缓存。
  • 分层缓存设计:结合 CDN、应用本地缓存(如 Guava Cache、Caffeine)、分布式缓存(如 Redis)形成分层架构,提升不同层次的访问性能、减少网络开销。
  • 预热与主动加载:在秒杀开始前将核心数据全部加载到缓存,避免高并发时缓存穿透或缓存击穿。
  • 缓存与业务逻辑深度结合:库存变更、下单流程、限流防刷等都需要依赖缓存进行快速决策与计数。
  • 异步落地与最终一致性:核心数据更新(库存、订单记录)不能全部同步写数据库,而是借助消息队列异步落地,以防 DB 冲击;同时需要设计保证最终一致性方案。

缓存体系架构

  1. CDN 静态资源缓存

    • 将秒杀页面的静态内容(HTML 静态化、CSS/JS、商品图片等)放到 CDN,用户直接访问静态页面或静态接口,减轻 Web 层压力。
    • 对动态接口(数据接口)可采用前端灰度控制:活动未开始状态下按钮置灰,不显示接口入口。
  2. 应用层本地缓存

    • 对热点数据(如秒杀开关、限流阈值、少量黑名单白名单)放在进程内缓存(Caffeine/Guava Cache)。读取极快,减少对 Redis 的调用次数。
    • 需要注意:本地缓存容量有限,适合少量常变配置或短期内不频繁更新的数据。更新时通过消息或主动失效机制通知各实例。
  3. 分布式缓存(Redis)

    • 商品信息缓存:秒杀商品详情,包含商品ID、名称、秒杀价格、开始/结束时间等。通常在秒杀前预加载到 Redis HASH 或简单 Key-Value。
    • 库存缓存:核心的秒杀库存计数,通常以单个 Key 记录剩余库存(整数)。
    • 访问记录缓存:记录 IP、用户请求次数、是否已下单等,用于防刷、限流、判重。
    • 布隆过滤器:保存所有合法秒杀商品 ID,防止恶意请求或错误 ID 访问穿透到 DB。可借助 RedisBloom、Guava Bloom 结合本地缓存等。
    • 黑名单/白名单缓存:提前识别可疑用户或优先用户。
    • 限流令牌/计数器:分布式限流时用 Redis 计数快速判断单用户或全局并发量。

缓存预热与缓存预加载

  • 商品信息预热

    • 在秒杀开始前,调度服务将所有秒杀商品信息加载到 Redis,例如:SET seckill:product:ID:info JSON(...)HSET seckill:products ID JSON
    • 本地缓存也可加载少量配置数据,如活动时间和阈值。
  • 库存预热

    • 将秒杀商品原始库存值加载到 Redis:SET seckill:product:ID:stock N。或使用 DECR 初始化前,使用 SECKILL_STOCK_ID = initialStock
  • 布隆过滤器预热

    • 构建合法商品 ID 的布隆过滤器,加载到 Redis 或应用本地,保证高并发检查时快速拒绝非法 ID 请求。
  • 访问记录/黑名单预热

    • 从历史数据中分析疑似刷单用户,将其 ID 缓存到 Redis SET 或本地缓存,在秒杀开始时即限制。

库存操作与缓存结合

  • 原子库存扣减

    • 使用 Redis 原子操作:DECRDECRBY。但需避免库存变为负数:可在 Lua 脚本中判断并扣减:

      local stock = tonumber(redis.call('GET', KEYS[1]))
      if not stock or stock <= 0 then
        return -1
      end
      redis.call('DECR', KEYS[1])
      return stock - 1
      
    • 该脚本可确保库存不会被超卖。

  • 本地内存计数优化

    • 若多实例并发压力极高,可在应用内维护一个本地剩余库存近似值,一旦 Redis 扣减失败,再走拒绝;本地计数可减少对 Redis 的访问,但需定期校正。
  • 预减库存思路

    • 在确认用户请求合法、限流通过后,先在缓存中原子预减库存,再异步将下单请求消息入队列供后续落地。
    • 若预减成功,则返回给用户“抢购成功,排队中”;若失败,立即返回“库存不足”。
  • 库存回滚

    • 在队列消费并落地时,如果因为用户超时未支付或其它原因需要回滚,应再 Redis 上执行原子增库存操作;需谨慎设计,避免与高并发预减冲突。
  • 可靠性保障

    • 结合消息队列确保预减库存与后续落地事务最终一致:如消费者处理失败时,要发送回滚消息或补偿。

防刷、限流与缓存

  • 布隆过滤器

    • 拦截非法商品 ID。
  • 访问记录缓存

    • Redis 中维护用户或 IP 的请求次数计数器(如每秒/每分钟请求数),超过阈值则拒绝;可用 Redis INCR + EXPIRE 实现滑动窗口近似限流。
  • 令牌桶/漏桶

    • 对全局或单用户并发请求总量限流,使用 Redis 简单计数或者更复杂的分布式令牌桶实现。
  • 用户状态缓存

    • 记录用户是否已成功下单,若已下单则直接拒绝重复请求。
  • 黑名单/白名单

    • 在秒杀前通过历史行为或风控系统导入可疑用户 ID 到 Redis SET,秒杀期间立即拒绝或降级处理。
  • 验证码或人机验证

    • 在热点时刻加验证码或滑动验证,缓存验证结果并短时记录,减少自动化脚本。

缓存一致性与失效

  • 缓存更新策略

    • 商品信息、配置类数据更新相对较少,可采用直接更新 Redis,并通过消息通知或发布-订阅让各实例清理本地缓存。
  • 库存缓存

    • 库存只在预热时加载且通过 Redis 原子操作变更,不需额外读取 DB 做强一致。要保证 Redis 不宕机:使用 Redis 集群或哨兵,保证高可用。
  • 延迟双删

    • 若需要同步更新 DB 或外部系统(如活动配额调整),在更新 DB 后删除缓存,再等待短暂时间后再次删除,防止并发更新时读到旧缓存。
  • 异步同步

    • 一般秒杀库存变更由缓存为主,DB 更新通过消息异步落地;若需要强一致性校验,可在流量低峰期做全量对账。
  • 缓存失效时间

    • 对于商品信息和活动配置可以设置较长 TTL 或使用永久缓存并在更新时主动更新,对限流计数器、访问记录计数器则使用短 TTL(如秒级或分钟级)。

异步落地与消息队列

  • 削峰填谷

    • 预减库存后,将下单请求封装成消息发送到消息队列(如 RocketMQ、Kafka、RabbitMQ)。消费者逐条消费处理:验证库存(可再次检查)、创建订单、写入数据库、调用支付等。
  • 失败重试与回滚

    • 消费失败:发送到死信队列或重新入队;若业务判断需回滚库存,发布回滚消息。
  • 最终一致性

    • 因异步,可能存在短暂不一致,但可借助定期对账、补偿机制保证库存数据和订单数据最终一致。
  • 监控消息积压

    • 实时监控队列堆积长度,若积压过多,可能需要临时扩容消费者或降级限流,防止积压过大导致超时。

监控与指标

  • 缓存相关指标

    • Redis 命中率、特定 Key 访问 QPS、内存使用、慢查询日志。
    • 本地缓存命中率、失效次数、缓存清理频次。
  • 限流与防刷指标

    • 拒绝请求数、限流触发率、异常请求来源统计。
  • 队列与落地

    • 队列长度、消费者处理速率、失败重试次数。
  • 业务质量指标

    • 成功下单率、支付转化率、库存扣减与订单创建比对、超卖事件监控。
  • 自动告警

    • 当 Redis 使用接近阈值、本地缓存异常、消息队列积压或消费者宕机时,及时告警并启动预案。

容灾与扩展

  • Redis 高可用

    • 使用 Redis 集群或主从 + 哨兵,保证单点故障可自动切换;多 AZ 部署。
  • 读写分离

    • 对某些只读查询场景(非核心秒杀流程)可以走只读节点,核心库存操作必须走主节点。
  • 本地缓存降级

    • 若 Redis 不可用,应用层需检测并降级:可返回系统繁忙提示或依靠本地缓存提供部分信息,避免直接崩溃。
  • 熔断和退避

    • 当下游(如数据库或消息队列)压力过大时,熔断部分非核心功能,优先保证秒杀核心流程;可动态调整限流策略。
  • 弹性扩缩容

    • 秒杀前预估容量,提前扩容实例和缓存集群;秒杀后快速缩容。

示例

伪代码示例(以 Java + SpringBoot + Lettuce/Jedis 为例):

  1. 库存预热:

    @Component
    public class SeckillPreload {
        @Autowired
        private StringRedisTemplate redisTemplate;
        @PostConstruct
        public void preload() {
            List<Product> products = fetchSeckillProductsFromDB();
            for (Product p : products) {
                String keyInfo = "seckill:product:" + p.getId() + ":info";
                redisTemplate.opsForValue().set(keyInfo, toJson(p));
                String keyStock = "seckill:product:" + p.getId() + ":stock";
                redisTemplate.opsForValue().set(keyStock, String.valueOf(p.getStock()));
                // 可设置TTL为活动结束后一段时间自动过期
                redisTemplate.expire(keyInfo, Duration.ofHours(2));
                redisTemplate.expire(keyStock, Duration.ofHours(2));
            }
            // 布隆过滤器预热略
        }
    }
    
  2. 预减库存 Lua 脚本:

    String lua = "local stock = tonumber(redis.call('GET', KEYS[1]));"
               + " if not stock or stock <= 0 then return -1; end;"
               + " redis.call('DECR', KEYS[1]);"
               + " return stock - 1;";
    // 执行时通过 RedisScript
    
  3. 下单接口(简化流程):

    @RestController
    public class SeckillController {
        @Autowired private StringRedisTemplate redisTemplate;
        @Autowired private RedisScript<Long> stockLuaScript;
        @Autowired private MessageQueueProducer mqProducer;
        @PostMapping("/seckill/{productId}")
        public ResponseEntity<?> seckill(@PathVariable Long productId, @RequestParam Long userId) {
            // 1. 活动时间检查(本地缓存或Redis)
            if (!isInSeckillTime(productId)) {
                return ResponseEntity.badRequest().body("活动未开始或已结束");
            }
            // 2. 布隆过滤器检查:略
            // 3. 防刷检查:检查用户请求频次、本地或Redis限流
            if (isUserBlacklisted(userId)) {
                return ResponseEntity.status(429).body("请求过于频繁");
            }
            // 4. 是否已下单检查
            String orderFlagKey = "seckill:order:flag:" + productId + ":" + userId;
            if (redisTemplate.hasKey(orderFlagKey)) {
                return ResponseEntity.badRequest().body("已参与过秒杀");
            }
            // 5. 预减库存
            String stockKey = "seckill:product:" + productId + ":stock";
            Long stockResult = redisTemplate.execute(stockLuaScript, Collections.singletonList(stockKey));
            if (stockResult == null || stockResult < 0) {
                return ResponseEntity.ok("秒杀已售罄");
            }
            // 6. 设置用户已下单标志(短期内有效,防重复)
            redisTemplate.opsForValue().set(orderFlagKey, "1", Duration.ofHours(1));
            // 7. 发送异步下单消息
            SeckillMessage msg = new SeckillMessage(userId, productId, System.currentTimeMillis());
            mqProducer.send(msg);
            // 8. 返回“排队中”或“已抢到,处理中”
            return ResponseEntity.ok("抢购成功,排队处理中");
        }
    }
    
  4. 消息消费者示例:

    @Component
    public class SeckillConsumer {
        @Autowired private OrderService orderService;
        @Autowired private InventoryService inventoryService;
        @Autowired private StringRedisTemplate redisTemplate;
        @RabbitListener(queues = "seckillQueue")
        public void process(SeckillMessage msg) {
            Long userId = msg.getUserId();
            Long productId = msg.getProductId();
            // 双重库存校验(可选)
            if (!inventoryService.checkStock(productId)) {
                // 回滚:增加 Redis 库存
                redisTemplate.opsForValue().increment("seckill:product:" + productId + ":stock");
                return;
            }
            // 创建订单、写 DB
            Order order = orderService.createOrder(userId, productId);
            // 支付等后续可异步
        }
    }
    
  5. 限流与防刷示例:

    • 使用 Redis INCR + EXPIRE 记录每个用户/IP 在短时间窗口内请求次数;超过阈值则拒绝。
    • 可结合本地缓存维护黑白名单及配置。

仅为思路演示,生产环境需考虑更全面的异常处理、超时、幂等、日志、追踪、分布式事务或补偿流程等。


小结

  • 秒杀前准备:评估流量峰值,扩容 Redis/应用实例、消息队列;完成缓存预热并压力测试;准备监控告警;风控名单预加载。
  • 秒杀中运行:实时监控缓存、队列、后端服务状态;动态调整限流策略;快速响应异常。
  • 秒杀后收尾:回顾队列积压与处理情况,对账库存与订单;清理临时缓存;总结经验。
  • 持续演进:基于日志和监控数据优化缓存策略、防刷算法、扩容策略;升级组件版本;优化运维自动化。

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到