【Redis】缓存击穿、缓存穿透、缓存雪崩的解决方案

【Redis】缓存击穿、缓存穿透、缓存雪崩的解决方案

在高并发业务中,Redis 缓存虽能大幅减轻数据库压力,但也会面临"缓存击穿""缓存穿透""缓存雪崩"三大典型问题------一旦爆发,可能导致数据库瞬间过载甚至宕机。

缓存击穿:高并发下热点 Key 失效

缓存击穿的核心场景是:某个被高并发访问的"热点 Key"突然过期失效,此时大量请求会绕过缓存直接冲击数据库,导致数据库压力骤增,甚至被压垮。

1. 方案一:互斥锁(分布式锁)

通过分布式锁保证"同一时间只有一个请求能访问数据库并更新缓存",其他请求需等待锁释放后再从缓存获取数据,本质是"限流削峰"。

执行流程:
  1. 请求查询缓存,若 Key 失效(缓存 miss),则尝试获取分布式锁(如 Redis 的 SET NX 命令);
  2. 若获取锁成功:访问数据库查询数据,将数据写入缓存(并设置新的过期时间),最后释放锁;
  3. 若获取锁失败:短暂休眠(如 50ms)后重新查询缓存,循环直到获取到数据;
  4. 优化 :加入"双重检测 "------获取锁后再次检查缓存是否已存在数据,避免因锁等待期间其他请求已更新缓存导致的重复操作。
优缺点:
  • 优势:数据一致性强(数据库更新后立即同步缓存),逻辑简单;
  • 劣势:存在锁等待阻塞,高并发下可能增加请求延迟,且需处理分布式锁的死锁、释放异常等问题。

2. 方案二:逻辑过期

不设置缓存 Key 的实际过期时间(让 Key 永不过期),而是在缓存数据内部嵌入"逻辑过期时间"。请求时通过判断逻辑时间决定是否异步更新缓存,避免阻塞。

执行流程:
  1. 缓存热点数据时,在 Value 中加入 expireTime(如 {data: "xxx", expireTime: 1699999999}),且 Key 不设置过期时间;
  2. 请求查询缓存时,先判断 expireTime 是否过期:
    • 若未过期:直接返回数据;
    • 若已过期:尝试获取分布式锁,获取成功后开启异步线程 (如线程池),去数据库更新数据并刷新缓存的 expireTime,同时释放锁;
    • 未获取到锁:直接返回旧数据(牺牲短暂一致性,保证性能)。
优缺点:
  • 优势:无请求阻塞,性能极高,适合对数据一致性要求不严格的场景(如商品销量、实时榜单);
  • 劣势:数据存在"短暂不一致"(过期后到异步更新完成前,返回旧数据)。

3. 方案三:中间件/框架方案

部分大厂会通过定制化中间件或框架,优化锁的性能开销,典型案例如下:

  • 某手 Cache Setter

    基于"一致性哈希"将相同 Key 的请求路由到同一台 Cache Setter 服务器,服务器内部用 ConcurrentHashMap 维护"Key-CountDownLatch"映射。当某 Key 首次缓存 miss 时,创建 CountDownLatch 并存储到 Map,同时去数据库更新缓存;后续请求查询到该 Key 对应的 Latch 时,会调用 await() 阻塞,直到第一个请求更新完缓存后调用 countDown(),所有阻塞请求唤醒并从缓存获取数据。

    核心优势:避免分布式锁的网络开销,用本地锁+Latch 实现"单节点内串行更新",性能更优。

  • 某东 Jd HotKey

    通过专门的热点 Key 检测系统,实时识别业务中的热点 Key,将这些 Key 同步到应用本地缓存(如 Caffeine)。当请求到来时,优先查询本地缓存,避免 Redis 热点 Key 失效后的数据库冲击。

    核心优势:本地缓存无网络延迟,且绕开了 Redis 层面的 Key 失效问题。

缓存穿透:不存在的 Key 冲击数据库

缓存穿透的核心场景是:请求查询的 Key 在缓存和数据库中都不存在(如恶意构造的无效 ID、不存在的用户信息),导致所有请求直接穿透缓存访问数据库,长期积累可能压垮数据库。

1. 方案一:请求参数校验与拦截

在请求到达缓存/数据库前,通过"网关层"或"业务层"拦截无效请求,从源头减少穿透。

  • 参数合法性校验:对请求参数做规则校验(如用户 ID 必须为正整数、订单号格式必须符合规则),非法参数直接返回 400;
  • IP/用户限流/黑名单:对高频发送无效请求的 IP 或用户(如每秒请求超 100 次),通过网关限流(如 Sentinel、Nginx 限流)或加入黑名单,短期内禁止其访问。

核心逻辑:在"缓存前"拦截无效请求,避免无效流量进入后续链路。

2. 方案二:缓存空值

对数据库中不存在的 Key,在缓存中存储"空值"(如 null""),并设置较短的过期时间(如 5-10 分钟),避免后续相同请求重复穿透。

执行流程:
  1. 请求查询 Key,缓存 miss 后访问数据库;
  2. 若数据库查询结果为空(数据不存在),则在缓存中写入 Key -> 空值,并设置短期过期时间;
  3. 后续相同 Key 的请求会从缓存获取空值,直接返回,无需访问数据库。
注意事项:
  • 过期时间不能过长:避免数据库后续新增该 Key 时,缓存的空值导致"数据不一致";
  • 需区分"真空"和"业务空":如"用户未下单"属于业务空,可缓存;"参数非法"属于无效请求,应直接拦截,无需缓存。

3. 方案三:布隆过滤器(Bloom Filter)

当请求的"不存在 Key"数量极多(如恶意构造百万级无效 ID),缓存空值会导致缓存膨胀,此时需用布隆过滤器做"前置存在性判断"------仅允许"可能存在"的数据进入缓存/数据库链路。

核心原理:
  • 写入阶段:数据库新增数据时,将 Key 通过多个哈希函数计算出多个哈希值,对应到布隆过滤器的"比特数组"中,将这些位置的比特位设为 1;
  • 查询阶段:请求到来时,先将 Key 通过相同哈希函数计算,若所有对应比特位均为 1,说明 Key"可能存在"(允许继续查缓存/数据库);若有任一比特位为 0,说明 Key"一定不存在"(直接返回,无需穿透)。
优缺点:
  • 优势:占用内存极小(比特级存储),判断速度快(O(k),k 为哈希函数个数),适合海量 Key 的存在性判断;
  • 劣势:存在"误判率"(可能将不存在的 Key 判断为存在),且不支持删除操作(删除一个 Key 会影响其他 Key 的判断)。

缓存雪崩:大量 Key 失效或 Redis 宕机

缓存雪崩的核心场景有两类:

  1. 大量 Key 同时过期:同一时间大量缓存 Key 失效,请求集中冲击数据库;
  2. Redis 整体不可用:Redis 宕机、内存溢出导致热点数据被淘汰,或机房故障等不可抗力,导致缓存完全失效,所有请求回源数据库。

1. 针对"大量 Key 同时过期"的解决方案

方案一:过期时间加随机值

在设置 Key 的固定过期时间(如 1 小时)基础上,额外叠加一个随机值(如 0-300 秒),让 Key 的实际过期时间分散在"1 小时 ~ 1 小时 5 分钟"之间,避免大量 Key 同一时间失效。

示例代码逻辑:

java 复制代码
// 固定过期时间 3600 秒,叠加 0-300 秒随机值
int baseExpire = 3600;
int randomExpire = new Random().nextInt(300);
redisTemplate.opsForValue().set(key, value, baseExpire + randomExpire, TimeUnit.SECONDS);
方案二:核心数据永不过期

对"几乎不变化的核心数据"(如城市列表、商品分类、配置信息),不设置过期时间,避免过期失效问题。若需更新数据,通过"异步线程"主动删除旧缓存并写入新数据(如数据库更新后,发送 MQ 消息触发缓存更新)。

方案三:热点数据缓存预热

在业务高峰到来前(如电商大促、直播带货开始前),通过脚本或后台任务提前将"预知的热点数据"(如热门商品、活动页面数据)加载到缓存中,并设置合理的过期时间(确保高峰期间不过期),避免高峰时缓存 miss。

2. 针对"Redis 整体不可用"的解决方案

方案一:Redis 高可用架构

通过"主从复制+哨兵"或"Redis Cluster 集群"保证 Redis 服务的可用性,避免单点故障:

  • 主从复制:主节点负责写操作,从节点负责读操作,主节点故障时从节点可切换为主节点;
  • 哨兵(Sentinel):实时监控主从节点健康状态,主节点宕机时自动完成故障转移;
  • Redis Cluster:将数据分片存储在多个节点,单个节点故障不影响整体服务,且支持水平扩容。
方案二:多机房容灾

对核心业务,采用"同城双活+异地灾备"架构:

  • 同城双活:同一城市部署两个机房的 Redis 集群,数据实时同步,单个机房故障时,流量可切换到另一个机房;
  • 异地灾备:不同城市部署灾备集群,定时同步数据(如每 5 分钟),极端情况下(如主城市机房被毁)可从灾备集群恢复服务。
方案三:服务兜底措施

即使 Redis 不可用,也要通过"服务降级、限流、熔断"避免数据库和业务服务被打垮:

  • 降级:缓存失效时,返回"默认数据"(如商品默认库存、历史推荐列表)或"服务暂时不可用"提示,减少数据库请求;
  • 限流:通过网关(如 Sentinel、Gateway)限制单位时间内的请求量(如每秒最多 1000 次请求到数据库),避免数据库过载;
  • 熔断:当数据库请求错误率超过阈值(如 50%)时,暂时熔断数据库访问(如 30 秒内不允许请求数据库),避免数据库雪上加霜,熔断后返回降级数据。

总结

缓存三大问题的本质都是"缓存失效导致请求回源数据库",解决方案的核心思路可归纳为三类:

  1. 限流:通过锁、中间件控制访问数据库的请求数(如击穿的互斥锁、雪崩的限流);
  2. 拦截:在请求到达数据库前过滤无效流量(如穿透的参数校验、布隆过滤器);
  3. 高可用:保证缓存服务稳定+数据不集中失效(如雪崩的高可用架构、过期时间随机化)。

实际业务中,需结合"数据一致性要求""并发量""业务场景"选择合适方案(如金融场景优先互斥锁,直播场景优先逻辑过期),必要时可组合多种方案(如布隆过滤器+缓存空值应对穿透),最大化保障系统稳定性。

相关推荐
一心赚狗粮的宇叔3 分钟前
中级软件开发工程师2025年度总结
java·大数据·oracle·c#
while(1){yan}10 分钟前
MyBatis Generator
数据库·spring boot·java-ee·mybatis
奋进的芋圆17 分钟前
DataSyncManager 详解与 Spring Boot 迁移指南
java·spring boot·后端
それども19 分钟前
MySQL affectedRows 计算逻辑
数据库·mysql
是小章啊27 分钟前
MySQL 之SQL 执行规则及索引详解
数据库·sql·mysql
计算机程序设计小李同学31 分钟前
个人数据管理系统
java·vue.js·spring boot·后端·web安全
富士康质检员张全蛋1 小时前
JDBC 连接池
数据库
yangminlei1 小时前
集成Camunda到Spring Boot项目
数据库·oracle
小途软件1 小时前
用于机器人电池电量预测的Sarsa强化学习混合集成方法
java·人工智能·pytorch·python·深度学习·语言模型
alonewolf_991 小时前
Spring MVC启动与请求处理全流程解析:从DispatcherServlet到HandlerAdapter
java·spring·mvc