文章目录
缓存全景图
Pre
分布式缓存:缓存设计中的 7 大经典问题_缓存失效、缓存穿透、缓存雪崩
分布式缓存:缓存设计中的 7 大经典问题_数据不一致与数据并发竞争
分布式缓存:缓存设计中的 7 大经典问题_Hot Key和Big Key
分布式缓存:ZSET → MGET 跨槽(cross‐slot)/ 并发 GET解决思路
背景与目标说明
短时间内流量剧增,后端需应对大量无效请求并快速响应成功请求。核心思路是:将绝大多数请求在上游拦截或由缓存处理,不让每个请求直击后端数据库和核心服务,同时保证库存操作原子、正确。缓存体系是关键环节,可极大提高查询速度、减轻数据库压力、提升用户体验。
缓存原则与设计思路
- 上游过滤、预处理优先:在到达核心库存/下单逻辑前,用缓存快速判断活动状态、库存是否售罄、用户是否有资格、是否已下单等,避免无效请求流向下游。
- 缓存优先、后端降级:常见数据(商品详情、秒杀配置信息)使用缓存读取;若缓存不可用或命中率低,再访问后端,并在适当时机补回缓存。
- 分层缓存设计:结合 CDN、应用本地缓存(如 Guava Cache、Caffeine)、分布式缓存(如 Redis)形成分层架构,提升不同层次的访问性能、减少网络开销。
- 预热与主动加载:在秒杀开始前将核心数据全部加载到缓存,避免高并发时缓存穿透或缓存击穿。
- 缓存与业务逻辑深度结合:库存变更、下单流程、限流防刷等都需要依赖缓存进行快速决策与计数。
- 异步落地与最终一致性:核心数据更新(库存、订单记录)不能全部同步写数据库,而是借助消息队列异步落地,以防 DB 冲击;同时需要设计保证最终一致性方案。
缓存体系架构
CDN 静态资源缓存
- 将秒杀页面的静态内容(HTML 静态化、CSS/JS、商品图片等)放到 CDN,用户直接访问静态页面或静态接口,减轻 Web 层压力。
- 对动态接口(数据接口)可采用前端灰度控制:活动未开始状态下按钮置灰,不显示接口入口。
应用层本地缓存
- 对热点数据(如秒杀开关、限流阈值、少量黑名单白名单)放在进程内缓存(Caffeine/Guava Cache)。读取极快,减少对 Redis 的调用次数。
- 需要注意:本地缓存容量有限,适合少量常变配置或短期内不频繁更新的数据。更新时通过消息或主动失效机制通知各实例。
分布式缓存(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,例如:
库存预热
- 将秒杀商品原始库存值加载到 Redis:
SET seckill:product:ID:stock N
。或使用DECR
初始化前,使用SECKILL_STOCK_ID = initialStock
。
- 将秒杀商品原始库存值加载到 Redis:
布隆过滤器预热
- 构建合法商品 ID 的布隆过滤器,加载到 Redis 或应用本地,保证高并发检查时快速拒绝非法 ID 请求。
访问记录/黑名单预热
- 从历史数据中分析疑似刷单用户,将其 ID 缓存到 Redis SET 或本地缓存,在秒杀开始时即限制。
库存操作与缓存结合
原子库存扣减
使用 Redis 原子操作:
DECR
或DECRBY
。但需避免库存变为负数:可在 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 为例):
库存预热:
@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)); } // 布隆过滤器预热略 } }
预减库存 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
下单接口(简化流程):
@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("抢购成功,排队处理中"); } }
消息消费者示例:
@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); // 支付等后续可异步 } }
限流与防刷示例:
- 使用 Redis INCR + EXPIRE 记录每个用户/IP 在短时间窗口内请求次数;超过阈值则拒绝。
- 可结合本地缓存维护黑白名单及配置。
仅为思路演示,生产环境需考虑更全面的异常处理、超时、幂等、日志、追踪、分布式事务或补偿流程等。
小结
- 秒杀前准备:评估流量峰值,扩容 Redis/应用实例、消息队列;完成缓存预热并压力测试;准备监控告警;风控名单预加载。
- 秒杀中运行:实时监控缓存、队列、后端服务状态;动态调整限流策略;快速响应异常。
- 秒杀后收尾:回顾队列积压与处理情况,对账库存与订单;清理临时缓存;总结经验。
- 持续演进:基于日志和监控数据优化缓存策略、防刷算法、扩容策略;升级组件版本;优化运维自动化。