Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Redis核心数据结构
  • Redis持久化机制
  • Redis高可用架构
  • Redis分布式锁
  • Redis缓存设计
    • 一、核心问题定义与本质区别
    • 二、缓存穿透:无效请求的防护方案
      • 2.1 解决方案1:接口参数校验(第一道防线)
      • 实现要点
      • 代码示例
      • 2.2 解决方案2:缓存空值(简单有效,中小场景首选)
      • 实现要点
      • 代码示例(Java + RedisTemplate)
      • 2.3 解决方案3:布隆过滤器(大数据量/高并发场景首选)
      • 核心特性
      • 实现方式:RedisBloom模块(生产环境推荐)
      • 步骤1:安装RedisBloom模块
      • 步骤2:核心代码实现(Java)
      • 误判率与内存占用参考
      • 2.4 缓存穿透方案组合建议
    • 三、缓存击穿:热点key的防护方案
      • 3.1 解决方案1:互斥锁(通用场景首选)
      • 实现要点
      • 代码示例(Java + Redisson)
      • 3.2 解决方案2:热点key永不过期(超高并发场景首选)
      • 方式1:物理永不过期(更新频率极低场景)
      • 方式2:逻辑永不过期(更新频率中等场景)
      • 代码示例(逻辑永不过期)
      • 3.3 解决方案3:逻辑过期+异步刷新(用户体验优先)
      • 实现要点
      • 代码示例
      • 3.4 缓存击穿方案组合建议
    • 四、缓存雪崩:大规模失效的防护方案
      • 4.1 解决方案1:过期时间随机化(基础方案,必选)
      • 实现要点
      • 代码示例
      • 4.2 解决方案2:多级缓存架构(提升可用性,推荐)
      • 实现要点
      • 代码示例(Caffeine + Redis)
      • 4.3 解决方案3:熔断降级与限流(保护数据库,兜底)
      • 实现示例(Sentinel限流 + 熔断)
      • 步骤1:添加依赖
      • 步骤2:配置限流规则与降级逻辑
      • 4.4 解决方案4:缓存高可用部署(避免单点故障,必选)
      • Redis Cluster 配置示例(Spring Boot)
      • 4.5 解决方案5:缓存预热(避免冷启动,推荐)
      • 实现要点
      • 代码示例(Spring Boot 启动预热)
      • 4.6 缓存雪崩方案组合建议
    • 五、实战最佳实践与监控告警
      • 5.1 核心参数配置参考
      • 5.2 数据一致性保障
      • 5.3 监控告警体系(提前预警)
      • 核心监控指标
      • 监控工具组合
      • 关键告警规则
    • 六、总结
  • Redis大Key与热Key
  • Redis限流
  • Redis IO多路复用
  • Redis过期删除策略
  • Redis Bitmap
  • 《Redis》笔记
Tavio
2023-04-27
目录

Redis缓存设计

在高并发系统中,Redis 作为核心缓存组件,能显著降低数据库压力、提升响应速度。但不合理的缓存设计易引发 穿透、击穿、雪崩 三大典型问题,可能导致数据库宕机、系统雪崩。

# 一、核心问题定义与本质区别

三大问题均源于“缓存未命中”,但触发场景、影响范围截然不同,需精准区分:

问题类型 定义 本质 典型场景 影响程度
缓存穿透 频繁请求“不存在的数据”,缓存无数据,请求直达数据库 缓存对“无效key”无防护,形成“缓存真空” 恶意攻击(遍历不存在的ID)、业务误操作(查询非法参数) 中等(持续消耗DB资源)
缓存击穿 热点key突然过期,瞬间大量并发请求未命中缓存,全部穿透到数据库 热点key“失效时间点”与“高并发请求”重叠,形成流量尖峰 秒杀商品缓存过期、热门文章缓存失效 高(瞬间压垮DB)
缓存雪崩 大量key同时过期,或缓存服务宕机,导致所有请求穿透到数据库 缓存层整体失效,流量集中冲击数据库 整点批量设置缓存过期、Redis 单点故障宕机 致命(系统整体雪崩)

# 二、缓存穿透:无效请求的防护方案

缓存穿透的核心是“无效key绕过缓存”,解决方案围绕“拦截无效请求”“填补缓存真空”展开,推荐三级防护体系,按需组合使用。

# 2.1 解决方案1:接口参数校验(第一道防线)

核心原理:在请求抵达缓存/数据库前,通过业务规则拦截非法参数,从源头阻断无效请求,成本最低、见效最快。

# 实现要点

  • 校验参数合法性:如用户ID、商品ID需为正整数,避免 -1、0、字符串等非法值。
  • 限制查询范围:如时间范围不能超过3个月,分页查询页码不超过1000页。
  • 黑名单拦截:维护恶意请求黑名单(如频繁查询无效ID的IP),直接拒绝。

# 代码示例

/**
 * 商品查询接口参数校验
 */
@RestController
@RequestMapping("/goods")
public class GoodsController {

    @Autowired
    private GoodsService goodsService;

    @GetMapping("/{id}")
    public Result<Goods> getGoods(@PathVariable Long id) {
        // 1. 基础参数校验:拦截非法ID
        if (id == null || id <= 0) {
            return Result.fail("商品ID必须为正整数");
        }
        // 2. 范围校验:假设商品ID最大为100万
        if (id > 1000000) {
            return Result.fail("商品ID超出合法范围");
        }
        // 3. 后续执行缓存查询逻辑
        return goodsService.getGoodsById(id);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 2.2 解决方案2:缓存空值(简单有效,中小场景首选)

核心原理:当数据库返回空结果时,在Redis中缓存“空值标记”并设置短期过期时间。后续请求会命中空值缓存,不再访问数据库,快速拦截无效请求。

# 实现要点

  • 空值需设置短期过期时间(5~10分钟):避免真实数据新增后,缓存空值导致查询不到(数据一致性兜底)。
  • 自定义NullValue标记类:避免与Redis默认不存储null的特性冲突,同时区分“真实空数据”和“缓存空标记”。
  • 结合参数校验使用:减少无效空值缓存的存储压力。

# 代码示例(Java + RedisTemplate)

@Service
public class GoodsService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private GoodsMapper goodsMapper;

    // 空值缓存过期时间:5分钟
    private static final long NULL_CACHE_EXPIRE = 5;
    // 正常数据缓存过期时间:30分钟
    private static final long NORMAL_CACHE_EXPIRE = 30;

    public Goods getGoodsById(Long id) {
        String cacheKey = "goods:id:" + id;

        // 1. 先查缓存
        Object cacheValue = redisTemplate.opsForValue().get(cacheKey);
        if (cacheValue != null) {
            // 命中空值标记,直接返回null
            if (cacheValue instanceof NullValue) {
                return null;
            }
            // 命中正常数据,直接返回
            return (Goods) cacheValue;
        }

        // 2. 缓存未命中,查询数据库
        Goods goods = goodsMapper.selectById(id);
        if (goods == null) {
            // 3. 数据库无数据:缓存空值标记
            redisTemplate.opsForValue().set(
                cacheKey, 
                new NullValue(), 
                NULL_CACHE_EXPIRE, 
                TimeUnit.MINUTES
            );
            return null;
        }

        // 4. 数据库有数据:缓存真实值
        redisTemplate.opsForValue().set(
            cacheKey, 
            goods, 
            NORMAL_CACHE_EXPIRE, 
            TimeUnit.MINUTES
        );
        return goods;
    }

    // 自定义空值标记类(需序列化)
    public static class NullValue implements Serializable {
        private static final long serialVersionUID = 1L;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 2.3 解决方案3:布隆过滤器(大数据量/高并发场景首选)

核心原理:布隆过滤器是概率性数据结构,通过“位数组+多个哈希函数”快速判断“元素是否可能存在于集合中”。不存在则直接返回,存在则走正常缓存流程,从根源拦截无效key。

# 核心特性

优点 缺点
空间利用率极高(以bit为单位存储) 存在误判率(无法完全避免穿透,需兜底)
查询速度快(O(k),k为哈希函数数量) 不支持删除数据(需定期重建)
支持海量数据(百万级元素仅需几MB内存) 需提前初始化合法数据集合

# 实现方式:RedisBloom模块(生产环境推荐)

RedisBloom是Redis的官方扩展模块,原生支持布隆过滤器,无需手动实现复杂逻辑。

# 步骤1:安装RedisBloom模块
# 1. 下载RedisBloom模块(GitHub:https://github.com/RedisBloom/RedisBloom)
# 2. 启动Redis时加载模块
redis-server --loadmodule /path/to/redisbloom.so
# 或在redis.conf中配置(永久生效)
loadmodule /path/to/redisbloom.so
1
2
3
4
5
# 步骤2:核心代码实现(Java)
@Service
public class BloomFilterService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 布隆过滤器名称(存储所有合法商品ID)
    private static final String BLOOM_FILTER_KEY = "bloom:filter:goods_ids";
    // 预期元素数量(100万)
    private static final long EXPECTED_SIZE = 1000000;
    // 误判率(1%,根据业务调整)
    private static final double FALSE_POSITIVE_RATE = 0.01;

    // 初始化布隆过滤器(系统启动时执行)
    @PostConstruct
    public void initBloomFilter() {
        // 检查过滤器是否已存在,不存在则创建
        Boolean exists = redisTemplate.hasKey(BLOOM_FILTER_KEY);
        if (Boolean.FALSE.equals(exists)) {
            // 执行bf.reserve命令:创建布隆过滤器
            redisTemplate.execute(
                new DefaultRedisScript<>(
                    "return bf.reserve(KEYS[1], ARGV[1], ARGV[2])",
                    String.class
                ),
                Collections.singletonList(BLOOM_FILTER_KEY),
                String.valueOf(FALSE_POSITIVE_RATE),
                String.valueOf(EXPECTED_SIZE)
            );
            // 批量添加合法商品ID(从数据库查询所有有效ID)
            List<Long> validGoodsIds = goodsMapper.selectAllValidIds();
            for (Long id : validGoodsIds) {
                addToBloomFilter(id.toString());
            }
            log.info("布隆过滤器初始化完成,加载合法商品ID {} 个", validGoodsIds.size());
        }
    }

    // 新增商品时,同步添加到布隆过滤器
    public void addToBloomFilter(String value) {
        redisTemplate.execute(
            new DefaultRedisScript<>("return bf.add(KEYS[1], ARGV[1])", Long.class),
            Collections.singletonList(BLOOM_FILTER_KEY),
            value
        );
    }

    // 判断元素是否可能存在(存在返回true,不存在返回false)
    public boolean mightContain(String value) {
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>("return bf.exists(KEYS[1], ARGV[1])", Long.class),
            Collections.singletonList(BLOOM_FILTER_KEY),
            value
        );
        return result != null && result == 1;
    }
}

// 业务层整合布隆过滤器
@Service
public class GoodsService {

    @Autowired
    private BloomFilterService bloomFilterService;

    public Goods getGoodsById(Long id) {
        String cacheKey = "goods:id:" + id;
        String bloomValue = id.toString();

        // 1. 布隆过滤器拦截:不存在则直接返回(第一道拦截)
        if (!bloomFilterService.mightContain(bloomValue)) {
            log.info("布隆过滤器拦截无效商品ID:{}", id);
            return null;
        }

        // 2. 查询缓存(第二道拦截,处理布隆误判)
        Object cacheValue = redisTemplate.opsForValue().get(cacheKey);
        if (cacheValue != null) {
            return cacheValue instanceof NullValue ? null : (Goods) cacheValue;
        }

        // 3. 查询数据库并缓存结果(省略,同缓存空值方案)
        return queryDbAndCacheResult(id, cacheKey);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# 误判率与内存占用参考
预期元素数量 误判率 内存占用(约)
10万 0.01% 190KB
100万 0.01% 1.8MB
1000万 0.01% 18MB
1000万 1% 12MB

# 2.4 缓存穿透方案组合建议

  • 中小场景:接口参数校验 + 缓存空值(低成本、易实现)。
  • 大数据量/高并发场景:接口参数校验 + 布隆过滤器 + 缓存空值(三重防护,零穿透)。

# 三、缓存击穿:热点key的防护方案

缓存击穿的核心是“热点key失效瞬间的并发冲击”,解决方案围绕“避免热点key失效”“控制并发查询”展开,优先保证高可用和用户体验。

# 3.1 解决方案1:互斥锁(通用场景首选)

核心原理:缓存失效时,仅允许一个线程获取锁查询数据库并更新缓存,其他线程等待重试,避免大量并发请求直接打数据库。

# 实现要点

  • 锁粒度:按热点key维度加锁(如lock:goods:1001),避免全局锁降低并发。
  • 锁过期时间:需大于“数据库查询+缓存更新”的总耗时(推荐3~5秒),避免锁提前释放导致并发问题。
  • 重试策略:非阻塞重试(最多5次,间隔100~200ms),避免线程长期阻塞。
  • 锁释放:必须在finally块中释放,且仅释放当前线程持有的锁。

# 代码示例(Java + Redisson)

@Service
public class GoodsService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private GoodsMapper goodsMapper;
    @Autowired
    private RedissonClient redissonClient;

    // 锁前缀(按key维度加锁)
    private static final String LOCK_PREFIX = "lock:goods:";
    // 缓存过期时间(30分钟)
    private static final long CACHE_EXPIRE = 30;
    // 锁过期时间(3秒,需大于DB查询+缓存更新耗时)
    private static final long LOCK_EXPIRE = 3;
    // 最大重试次数
    private static final int MAX_RETRY = 5;
    // 重试间隔(100ms)
    private static final long RETRY_INTERVAL = 100;

    public Goods getGoodsById(Long id) {
        String cacheKey = "goods:id:" + id;
        String lockKey = LOCK_PREFIX + id;

        // 1. 先查缓存:命中直接返回
        Goods goods = (Goods) redisTemplate.opsForValue().get(cacheKey);
        if (goods != null) {
            return goods;
        }

        // 2. 缓存失效,获取互斥锁
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁:非阻塞(最多等待0秒),锁3秒后自动释放
            boolean locked = lock.tryLock(0, LOCK_EXPIRE, TimeUnit.SECONDS);
            if (locked) {
                // 3. 获锁成功:查询数据库(双重检查缓存,避免锁等待期间缓存已更新)
                goods = (Goods) redisTemplate.opsForValue().get(cacheKey);
                if (goods != null) {
                    return goods;
                }
                // 数据库查询
                goods = goodsMapper.selectById(id);
                if (goods == null) {
                    // 数据库无数据:缓存空值(5分钟)
                    redisTemplate.opsForValue().set(
                        cacheKey, 
                        new NullValue(), 
                        5, 
                        TimeUnit.MINUTES
                    );
                    return null;
                }
                // 数据库有数据:更新缓存
                redisTemplate.opsForValue().set(
                    cacheKey, 
                    goods, 
                    CACHE_EXPIRE, 
                    TimeUnit.MINUTES
                );
                return goods;
            } else {
                // 4. 获锁失败:重试(最多5次)
                int retryCount = 0;
                while (retryCount < MAX_RETRY) {
                    Thread.sleep(RETRY_INTERVAL);
                    goods = (Goods) redisTemplate.opsForValue().get(cacheKey);
                    if (goods != null) {
                        return goods;
                    }
                    retryCount++;
                }
                // 重试失败:返回兜底数据(保证用户体验)
                return buildFallbackGoods(id);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return buildFallbackGoods(id);
        } finally {
            // 5. 释放锁(仅释放当前线程持有的锁)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    // 构建兜底数据
    private Goods buildFallbackGoods(Long id) {
        Goods fallback = new Goods();
        fallback.setId(id);
        fallback.setName("系统繁忙,请稍后再试");
        fallback.setPrice(BigDecimal.ZERO);
        return fallback;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

# 3.2 解决方案2:热点key永不过期(超高并发场景首选)

核心原理:不让热点key自动过期,从根源上避免失效瞬间的并发冲击,分为“物理永不过期”和“逻辑永不过期”两种方式。

# 方式1:物理永不过期(更新频率极低场景)

  • 实现:缓存不设置EXPIRE时间,热点数据永久存储在Redis中。
  • 数据更新:数据库更新后,手动调用SET命令覆盖缓存(如通过消息队列异步更新)。
  • 适用场景:首页Banner、固定配置、更新频率极低的热点数据(如年度爆款商品)。

# 方式2:逻辑永不过期(更新频率中等场景)

  • 实现:缓存设置物理过期时间(如30分钟),同时启动后台异步线程,定期(如29分钟)更新过期时间,保证“逻辑上不过期”。
  • 优势:兼顾可用性和数据一致性,即使后台线程故障,物理过期时间仍能兜底。

# 代码示例(逻辑永不过期)

@Service
public class HotKeyRefreshService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 热点key列表(可从配置中心/数据库动态获取)
    private static final List<String> HOT_KEYS = Arrays.asList(
        "goods:id:1001", "goods:id:1002", "goods:id:1003"
    );

    // 定时任务:每29分钟刷新一次过期时间(Spring Schedule)
    @Scheduled(fixedRate = 29 * 60 * 1000)
    @Async // 异步执行,避免阻塞主线程
    public void refreshHotKeyExpire() {
        log.info("开始刷新热点key过期时间");
        for (String key : HOT_KEYS) {
            // 检查key是否存在,存在则延长过期时间至30分钟
            Boolean exists = redisTemplate.hasKey(key);
            if (Boolean.TRUE.equals(exists)) {
                redisTemplate.expire(key, 30, TimeUnit.MINUTES);
                log.info("刷新热点key:{} 过期时间至30分钟", key);
            }
        }
    }
}

// 热点key存储逻辑
public void cacheHotGoods(Goods goods) {
    String cacheKey = "goods:id:" + goods.getId();
    // 设置物理过期时间30分钟
    redisTemplate.opsForValue().set(cacheKey, goods, 30, TimeUnit.MINUTES);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 3.3 解决方案3:逻辑过期+异步刷新(用户体验优先)

核心原理:缓存存储“数据+逻辑过期时间”,即使物理过期,仍返回旧数据,同时异步线程后台更新缓存,兼顾可用性和数据新鲜度(无等待时间)。

# 实现要点

  • 缓存封装:自定义CacheWrapper类,包含真实数据和逻辑过期时间。
  • 物理过期时间:设为逻辑过期时间的2倍(兜底,避免缓存永久无效)。
  • 异步更新:逻辑过期后,返回旧数据,同时异步获取锁更新缓存,避免并发更新。

# 代码示例

// 1. 封装缓存对象(含逻辑过期时间)
@Data
public class CacheWrapper<T> implements Serializable {
    private T data; // 真实数据
    private long logicExpireTime; // 逻辑过期时间(毫秒)

    // 判断是否逻辑过期
    public boolean isExpired() {
        return System.currentTimeMillis() > logicExpireTime;
    }
}

// 2. 业务层实现
@Service
public class GoodsService {

    @Autowired
    private RedissonClient redissonClient;

    public Goods getGoodsById(Long id) {
        String cacheKey = "goods:id:" + id;
        String lockKey = LOCK_PREFIX + id;

        // 1. 查询缓存
        CacheWrapper<Goods> cacheWrapper = (CacheWrapper<Goods>) redisTemplate.opsForValue().get(cacheKey);
        if (cacheWrapper == null) {
            // 缓存未命中:走互斥锁查询DB(同3.1方案,省略)
            return queryDbWithLock(id, cacheKey);
        }

        Goods goods = cacheWrapper.getData();
        // 2. 逻辑未过期:直接返回数据
        if (!cacheWrapper.isExpired()) {
            return goods;
        }

        // 3. 逻辑过期:返回旧数据,异步更新缓存(不阻塞用户)
        CompletableFuture.runAsync(() -> {
            RLock lock = redissonClient.getLock(lockKey);
            try {
                // 尝试获取锁:非阻塞,避免并发更新
                if (lock.tryLock(0, LOCK_EXPIRE, TimeUnit.SECONDS)) {
                    log.info("异步更新热点key缓存:{}", cacheKey);
                    // 查询最新数据
                    Goods newGoods = goodsMapper.selectById(id);
                    // 构建新缓存对象
                    CacheWrapper<Goods> newWrapper = new CacheWrapper<>();
                    newWrapper.setData(newGoods);
                    // 逻辑过期时间:30分钟后
                    newWrapper.setLogicExpireTime(System.currentTimeMillis() + 30 * 60 * 1000);
                    // 物理过期时间:60分钟(兜底)
                    redisTemplate.opsForValue().set(cacheKey, newWrapper, 60, TimeUnit.MINUTES);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("异步更新缓存失败,key:{}", cacheKey, e);
            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        });

        // 直接返回旧数据,保证用户体验
        return goods;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

# 3.4 缓存击穿方案组合建议

  • 通用场景:互斥锁(兼顾一致性和并发)。
  • 超高并发场景:逻辑永不过期 + 异步刷新(优先保证可用性)。
  • 用户体验优先场景:逻辑过期 + 异步刷新(无等待时间)。

# 四、缓存雪崩:大规模失效的防护方案

缓存雪崩的核心是“缓存层整体失效”,解决方案围绕“分散失效时间”“提升缓存可用性”“限制数据库压力”展开,构建多层防护体系。

# 4.1 解决方案1:过期时间随机化(基础方案,必选)

核心原理:为每个缓存key设置“基础过期时间+随机偏移量”,避免大量key在同一时间点过期,分散流量峰值。

# 实现要点

  • 基础过期时间:根据数据热度设置(热数据24小时,温数据30分钟,冷数据10分钟)。
  • 随机偏移量:热数据偏移量大(1~2小时),冷数据偏移量小(5~10分钟),避免偏移后仍集中过期。
  • 代码封装:统一工具类生成随机过期时间,避免重复代码。

# 代码示例

@Component
public class CacheTtlUtil {

    // 随机数生成器
    private static final Random RANDOM = new Random();

    /**
     * 生成随机过期时间(秒)
     * @param baseTtl 基础过期时间(秒)
     * @param level 数据热度等级
     * @return 随机过期时间(秒)
     */
    public int getRandomTtl(int baseTtl, HotspotLevel level) {
        int randomRange = switch (level) {
            case HIGH -> 2 * 3600; // 热数据:0~2小时偏移
            case MEDIUM -> 3600; // 温数据:0~1小时偏移
            case LOW -> 600; // 冷数据:0~10分钟偏移
        };
        // 随机偏移量:0 ~ randomRange 秒
        return baseTtl + RANDOM.nextInt(randomRange);
    }

    // 数据热度等级枚举
    public enum HotspotLevel {
        HIGH, MEDIUM, LOW
    }
}

// 使用示例
@Service
public class GoodsService {

    @Autowired
    private CacheTtlUtil cacheTtlUtil;

    public void cacheGoods(Goods goods, CacheTtlUtil.HotspotLevel level) {
        String cacheKey = "goods:id:" + goods.getId();
        // 基础过期时间:热数据24小时,温数据30分钟,冷数据10分钟
        int baseTtl = switch (level) {
            case HIGH -> 24 * 3600;
            case MEDIUM -> 30 * 60;
            case LOW -> 10 * 60;
        };
        // 生成随机过期时间
        int randomTtl = cacheTtlUtil.getRandomTtl(baseTtl, level);
        // 缓存数据
        redisTemplate.opsForValue().set(cacheKey, goods, randomTtl, TimeUnit.SECONDS);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 4.2 解决方案2:多级缓存架构(提升可用性,推荐)

核心原理:构建“本地缓存(JVM)+ Redis”的二级缓存,即使Redis宕机,本地缓存仍能承接部分流量,避免缓存层整体失效。

# 实现要点

  • 本地缓存选型:推荐Caffeine(高性能、支持过期策略),避免使用HashMap(无过期和淘汰机制)。
  • 本地缓存配置:最大容量1~10万(避免占用过多JVM内存),过期时间2~5分钟(短于Redis缓存,保证数据一致性)。
  • 一致性保障:数据库更新时,先更新Redis,再删除本地缓存(或通过消息队列通知所有节点删除本地缓存)。

# 代码示例(Caffeine + Redis)

// 1. 配置本地缓存(Spring Boot)
@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<String, Object> localCache() {
        return Caffeine.newBuilder()
                .maximumSize(10000) // 最大缓存数量(根据内存调整)
                .expireAfterWrite(3, TimeUnit.MINUTES) // 本地缓存过期时间:3分钟
                .recordStats() // 开启统计(可选)
                .build();
    }
}

// 2. 业务层实现二级缓存
@Service
public class GoodsService {

    @Autowired
    private Cache<String, Object> localCache;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public Goods getGoodsById(Long id) {
        String cacheKey = "goods:id:" + id;

        // 1. 先查本地缓存(最快)
        Object localValue = localCache.getIfPresent(cacheKey);
        if (localValue != null) {
            return localValue instanceof NullValue ? null : (Goods) localValue;
        }

        // 2. 再查Redis缓存
        Object redisValue = redisTemplate.opsForValue().get(cacheKey);
        if (redisValue != null) {
            // 更新本地缓存(同步数据)
            localCache.put(cacheKey, redisValue);
            return redisValue instanceof NullValue ? null : (Goods) redisValue;
        }

        // 3. 最后查数据库并更新缓存(省略,同前)
        Goods goods = queryDbAndCacheResult(id, cacheKey);
        localCache.put(cacheKey, goods == null ? new NullValue() : goods);
        return goods;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

# 4.3 解决方案3:熔断降级与限流(保护数据库,兜底)

核心原理:当Redis宕机或数据库压力过大时,通过熔断降级限制请求量,返回兜底数据,避免数据库被压垮。

# 实现示例(Sentinel限流 + 熔断)

# 步骤1:添加依赖
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.6</version>
</dependency>
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-annotation-aspectj</artifactId>
    <version>1.8.6</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
# 步骤2:配置限流规则与降级逻辑
// 1. 初始化Sentinel限流规则(系统启动时)
@Configuration
public class SentinelConfig {

    @PostConstruct
    public void initFlowRule() {
        // 数据库查询接口限流规则:QPS上限1000(根据DB承载能力调整)
        FlowRule flowRule = new FlowRule();
        flowRule.setResource("goodsDbQuery"); // 资源名(与@SentinelResource注解一致)
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 按QPS限流
        flowRule.setCount(1000); // QPS上限
        FlowRuleManager.loadRules(Collections.singletonList(flowRule));

        // 熔断规则:异常比例超过50%,熔断10秒
        DegradeRule degradeRule = new DegradeRule();
        degradeRule.setResource("goodsDbQuery");
        degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
        degradeRule.setCount(0.5); // 异常比例阈值
        degradeRule.setTimeWindow(10); // 熔断时间(秒)
        DegradeRuleManager.loadRules(Collections.singletonList(degradeRule));
    }
}

// 2. 业务层添加Sentinel注解
@Service
public class GoodsService {

    @Autowired
    private GoodsMapper goodsMapper;

    // 数据库查询方法:添加限流和熔断注解
    @SentinelResource(
        value = "goodsDbQuery",
        fallback = "dbQueryFallback", // 降级兜底方法
        blockHandler = "dbQueryBlockHandler" // 限流阻塞处理方法
    )
    public Goods queryDb(Long id) {
        return goodsMapper.selectById(id);
    }

    // 降级兜底方法(异常时触发)
    public Goods dbQueryFallback(Long id, Throwable e) {
        log.error("数据库查询异常,id:{}", id, e);
        return buildFallbackGoods(id);
    }

    // 限流阻塞处理方法(QPS超限时触发)
    public Goods dbQueryBlockHandler(Long id, BlockException e) {
        log.error("数据库查询限流,id:{}", id, e);
        return buildFallbackGoods(id);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 4.4 解决方案4:缓存高可用部署(避免单点故障,必选)

核心方案:采用Redis Cluster或“主从+哨兵”架构,确保缓存服务无单点故障,即使部分节点宕机,仍能提供服务。

部署方案 架构说明 优势 适用场景 部署成本
主从+哨兵 1主N从,哨兵监控主节点,故障自动切换 部署简单、成本低、兼容性好 中小规模系统 低
Redis Cluster 3主3从,数据自动分片,故障自愈 高可用、可横向扩展、支持海量数据 大规模、高并发系统 中

# Redis Cluster 配置示例(Spring Boot)

spring:
  redis:
    cluster:
      nodes: 192.168.1.101:6379,192.168.1.102:6379,192.168.1.103:6379,192.168.1.104:6379,192.168.1.105:6379,192.168.1.106:6379
      max-redirects: 3 # 最大重定向次数(集群分片跳转)
    lettuce:
      pool:
        max-active: 32 # 连接池最大活跃数
        max-idle: 8 # 连接池最大空闲数
        min-idle: 4 # 连接池最小空闲数
    timeout: 3000 # 连接超时时间(毫秒)
1
2
3
4
5
6
7
8
9
10
11

# 4.5 解决方案5:缓存预热(避免冷启动,推荐)

核心原理:系统启动时,主动加载热点数据到缓存,避免冷启动期间大量请求穿透到数据库(如秒杀系统启动后,秒杀商品缓存为空)。

# 实现要点

  • 预热数据:仅加载热点数据(如秒杀商品、首页热门商品),避免全量数据预热占用过多内存。
  • 预热时机:系统启动后(通过CommandLineRunner或ApplicationRunner),或流量低峰期(如凌晨2点)。
  • 异步预热:批量数据预热采用异步线程池,避免阻塞系统启动。

# 代码示例(Spring Boot 启动预热)

@Component
public class CacheWarmer implements CommandLineRunner {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private GoodsMapper goodsMapper;
    @Autowired
    private CacheTtlUtil cacheTtlUtil;

    // 热点数据:秒杀商品ID列表(可从配置中心动态获取)
    private static final List<Long> HOT_SECKILL_GOODS_IDS = Arrays.asList(1001L, 1002L, 1003L);

    @Override
    public void run(String... args) throws Exception {
        // 异步预热热点数据(避免阻塞系统启动)
        CompletableFuture.runAsync(this::warmUpSeckillGoods);
    }

    // 预热秒杀商品缓存
    private void warmUpSeckillGoods() {
        log.info("开始预热秒杀商品缓存,共{}个商品", HOT_SECKILL_GOODS_IDS.size());
        for (Long id : HOT_SECKILL_GOODS_IDS) {
            try {
                Goods goods = goodsMapper.selectById(id);
                String cacheKey = "goods:id:" + id;
                // 设置随机过期时间(24小时~26小时)
                int ttl = cacheTtlUtil.getRandomTtl(24 * 3600, CacheTtlUtil.HotspotLevel.HIGH);
                redisTemplate.opsForValue().set(cacheKey, goods, ttl, TimeUnit.SECONDS);
                log.info("预热商品缓存成功,ID:{}", id);
            } catch (Exception e) {
                log.error("预热商品缓存失败,ID:{}", id, e);
            }
        }
        log.info("秒杀商品缓存预热完成");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 4.6 缓存雪崩方案组合建议

  • 基础组合:过期时间随机化 + Redis高可用部署(避免大部分雪崩场景)。
  • 高可用组合:过期时间随机化 + 多级缓存 + 熔断降级 + Redis Cluster(核心系统首选)。
  • 冷启动防护:缓存预热(秒杀、大促场景必选)。

# 五、实战最佳实践与监控告警

# 5.1 核心参数配置参考

参数类型 推荐值 说明
缓存空值过期时间 5~10分钟 平衡内存占用与数据新鲜度
布隆过滤器误判率 0.01%~1% 百万级数据推荐1%(内存占用低)
互斥锁过期时间 3~5秒 大于DB查询+缓存更新总耗时(预留冗余)
本地缓存过期时间 2~5分钟 短于Redis缓存,避免数据不一致
本地缓存最大容量 1~10万 根据JVM内存调整(避免OOM)
热点key缓存时间 24小时(逻辑永不过期) 结合异步续期机制
限流QPS阈值 数据库最大承载QPS的80% 预留缓冲空间

# 5.2 数据一致性保障

  • 缓存更新策略:推荐“更新数据库 + 删除缓存”(Cache-Aside模式),避免“更新缓存 + 更新数据库”的一致性问题。
  • 延迟删除:删除缓存时可添加短暂延迟(如100ms),解决“数据库更新未完成,缓存已被读取”的竞态条件。
  • 分布式场景:使用消息队列通知所有节点删除本地缓存,保证集群节点数据一致。

# 5.3 监控告警体系(提前预警)

# 核心监控指标

  • 缓存层:缓存命中率(阈值≥90%)、Redis内存使用率(阈值≤85%)、Redis节点存活状态、缓存过期key数量。
  • 数据库层:DB QPS(监控突发增长)、DB慢查询次数(阈值≤10次/分钟)、DB连接数(阈值≤最大连接数的80%)。
  • 业务层:接口响应时间(阈值≤500ms)、降级兜底触发次数(阈值≤100次/分钟)。

# 监控工具组合

  • 采集:Prometheus + Redis Exporter + Spring Boot Actuator。
  • 可视化:Grafana(配置仪表盘,实时查看指标)。
  • 告警:AlertManager(支持邮件、短信、钉钉告警)。

# 关键告警规则

  • 缓存命中率低于90%(持续5分钟)。
  • Redis节点宕机(持续1分钟)。
  • DB QPS突增50%以上(持续3分钟)。
  • 接口响应时间超过1秒(持续5分钟)。

# 六、总结

Redis缓存三大问题的本质是“缓存未命中后的流量失控”,解决方案的核心思路是:提前拦截无效请求、控制并发查询流量、提升缓存可用性、保护数据库底线。

问题类型 核心解决方案 推荐组合
缓存穿透 布隆过滤器、缓存空值 接口校验 + 布隆过滤器 + 缓存空值
缓存击穿 互斥锁、逻辑永不过期 互斥锁 + 逻辑过期异步刷新
缓存雪崩 随机TTL、多级缓存、高可用部署 随机TTL + 多级缓存 + Redis Cluster + 熔断降级

实际落地时,需根据业务场景灵活选择方案:中小规模系统可优先使用“缓存空值+随机TTL+互斥锁”的轻量方案;大规模高并发系统则需组合“布隆过滤器+多级缓存+Redis Cluster+熔断降级”的全方位防护体系。

优先使用Redisson、RedisBloom、Caffeine等成熟框架,避免重复造轮子,同时通过监控告警提前发现风险,确保缓存体系稳定可靠。

编辑 (opens new window)
#缓存穿透#缓存击穿#缓存雪崩
上次更新: 2026/01/21, 19:29:14
Redis分布式锁
Redis大Key与热Key

← Redis分布式锁 Redis大Key与热Key→

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式