缓存更新策略
在 Redis 缓存中,缓存的更新策略主要有**定期生成(定时更新)和实时生成(即时更新)**两种方式。不同的策略适用于不同的业务场景,涉及性能、数据一致性和系统负载等方面的权衡。
1. 定期生成(定时更新)
是什么?
定期生成指的是按照固定的时间间隔,主动更新缓存,而不是在数据发生变化时立即更新。这种方式适用于数据变化不频繁、对实时性要求不高的场景。
优点:
✅ 降低数据库压力:缓存可以批量更新,避免频繁查询数据库。
✅ 提高查询性能:查询时直接读取缓存,响应速度快。
✅ 数据一致性较好(相对于长期不更新的缓存):定期更新可以保证数据不会长期过时。
缺点:
❌ 数据可能不够实时:在缓存下一次更新前,数据可能已经变化,但缓存仍然返回旧数据。
❌ 不适合高实时性业务:如果业务需要频繁变更数据,定期更新可能导致缓存数据滞后。
❌ 可能会引起短时流量冲击:如果所有缓存数据同时更新,可能会对数据库造成瞬间压力。
常见实现方式:
-
定时任务更新缓存(Time-based Refresh)
- 使用 Spring Task、Quartz、Crontab 等定时任务,每隔一段时间刷新缓存。
- 例如,每 10 分钟更新一次缓存:
@Scheduled(fixedRate = 600000) // 每 10 分钟执行一次 public void updateCache() {// 查询数据库并更新缓存List<Data> dataList = databaseService.getData();redisTemplate.opsForValue().set("cache:data", dataList); }
-
数据库变更时触发缓存更新(Database-triggered Refresh)
- 监听 数据库变更事件(MySQL Binlog、PostgreSQL 触发器),检测到数据变化后批量刷新缓存。
-
异步任务更新
- 使用消息队列(Kafka、RabbitMQ)通知服务更新缓存,避免定时任务导致的瞬时数据库压力过大。
适用场景:
📌 统计数据、排行榜、热门商品列表等(更新频率较低,数据稍有延迟也无大问题)。
📌 日志分析、报表数据等(数据量大,但对实时性要求不高)。
2. 实时生成(即时更新)
是什么?
实时生成指的是数据发生变更时立即更新缓存,确保缓存数据始终是最新的。这种方式适用于对数据一致性要求高、变更较频繁的场景。
优点:
✅ 数据实时性高:缓存的数据始终与数据库保持一致,适用于高实时性需求的应用。
✅ 避免缓存不一致问题:数据库变更后立即同步缓存,减少数据不匹配的情况。
缺点:
❌ 更新成本高:每次数据变更都需要更新缓存,可能会导致数据库压力增大。
❌ 可能导致缓存频繁更新:对于高频变更的数据,频繁更新可能会导致 Redis 负载过重,甚至影响整体性能。
❌ 并发问题:多个并发请求可能会导致缓存不一致或缓存击穿,需要加锁或使用双写策略。
常见实现方式:
-
数据库更新时主动更新缓存(Write-through Strategy)
- 在 **数据更新(新增、修改、删除)**时,同时更新数据库和缓存:
public void updateData(Data data) {databaseService.updateData(data); // 更新数据库redisTemplate.opsForValue().set("cache:data:" + data.getId(), data); // 同步更新缓存 }
- 适用于数据变更不频繁,且一致性要求较高的场景。
- 在 **数据更新(新增、修改、删除)**时,同时更新数据库和缓存:
-
缓存淘汰(Cache Eviction)
- 在数据库更新后,删除缓存,让下一次查询时重新加载数据:
public void updateData(Data data) {databaseService.updateData(data); // 更新数据库redisTemplate.delete("cache:data:" + data.getId()); // 删除缓存 }
- 适用于缓存数据不是热点,数据变更后不需要立即被查询的情况。
- 在数据库更新后,删除缓存,让下一次查询时重新加载数据:
-
订阅数据库变更(Event-based Strategy)
- 使用 消息队列(Kafka、RabbitMQ) 或 Redis 订阅/发布机制,监听数据库变更事件,变更后更新缓存。
-
分布式锁(避免缓存并发写入问题)
- 解决多个请求同时更新缓存导致数据不一致的问题:
RLock lock = redissonClient.getLock("cache:lock:data:" + data.getId()); try {if (lock.tryLock(5, TimeUnit.SECONDS)) {databaseService.updateData(data);redisTemplate.opsForValue().set("cache:data:" + data.getId(), data);} } finally {lock.unlock(); }
- 适用于高并发写入场景,防止缓存同时被多个请求覆盖。
- 解决多个请求同时更新缓存导致数据不一致的问题:
适用场景:
📌 订单系统、支付系统、库存管理等(数据必须实时更新,不能有延迟)。
📌 直播、弹幕系统(数据实时变化,需要确保一致性)。
总结:定期生成 vs. 实时生成
策略 | 定期生成(定时更新) | 实时生成(即时更新) |
---|---|---|
数据实时性 | 低(有一定延迟) | 高(数据库更新即缓存更新) |
数据库压力 | 低(定期批量更新) | 高(频繁更新缓存) |
缓存命中率 | 高(查询时直接命中缓存) | 可能较低(某些情况需删除缓存) |
适用场景 | 排行榜、统计数据、报表等 | 订单、库存、支付等高一致性业务 |
总结
- 定期生成(定时更新) 适用于数据变化不频繁、对实时性要求不高的场景,如排行榜、日志分析等。
- 实时生成(即时更新) 适用于数据变化频繁、对一致性要求高的场景,如支付、库存、订单管理等。
- 在实际应用中,可以结合两种策略,例如:
- 定期更新 + 变更触发更新:大部分数据定期刷新,关键数据实时更新。
- 读时更新 + 写时淘汰:查询时自动更新缓存,写入时删除缓存,防止数据不一致。
合理选择缓存更新策略,可以有效提升系统性能,降低数据库压力,并保证数据的一致性。
Redis 作为缓存,存储空间有限,因此需要淘汰数据来保证新数据的存入。Redis 提供了多种缓存淘汰策略(Eviction Policy),用于决定哪些数据需要被删除。下面介绍几种常见的淘汰策略,包括它们的适用场景和优缺点。
缓存淘汰策略
1. 不淘汰策略
1.1 noeviction(拒绝写入)
概念:
当 Redis 内存占满时,不会删除任何已有数据,而是直接返回错误,拒绝新的写入请求。
适用场景:
- 适用于严格不能丢数据的场景,如任务队列(消息队列)、金融交易等。
- 适用于 Redis 作为纯数据存储而非缓存时。
优缺点:
✅ 数据不会被误删除,保证数据完整性。
❌ 可能导致写入失败,影响系统稳定性。
2. 基于 TTL(过期时间)的淘汰策略
2.1 volatile-lru(最近最少使用,TTL 限定)
概念:
- 只淘汰**设置了过期时间(TTL)**的键。
- 在这些键中,优先删除最近最少使用(LRU, Least Recently Used)的数据。
适用场景:
- 适用于部分数据可丢弃的场景,比如 session、短期缓存数据。
- 适用于需要自动过期控制,但仍希望尽可能保留热点数据的情况。
优缺点:
✅ 优先保留常用数据,减少缓存击穿的概率。
❌ 如果大部分 key 没有 TTL,可能导致 Redis 直接拒绝写入(相当于 noeviction)。
2.2 volatile-ttl(优先淘汰即将过期的键)
概念:
- 只淘汰**设置了过期时间(TTL)**的键。
- 其中剩余寿命最短的键优先被删除。
适用场景:
- 适用于对数据有明确的生命周期需求的业务,如订单缓存、验证码缓存等。
优缺点:
✅ 优先删除即将过期的数据,保证短期缓存的更新。
❌ 可能误删仍然有价值的热点数据。
3. 基于数据访问频率的淘汰策略
3.1 allkeys-lru(全局最近最少使用)
概念:
- 无视 TTL,从所有键中(包括没有设置 TTL 的键),优先淘汰最近最少使用的键。
适用场景:
- 适用于热点数据更新频繁的场景,如推荐系统、排行榜、搜索结果缓存等。
优缺点:
✅ 可以确保常用数据长期保留,提高缓存命中率。
❌ 如果热点数据突然减少访问,可能会被错误淘汰。
3.2 allkeys-random(全局随机淘汰)
概念:
- 无视 TTL,在所有 key 中随机删除某些数据。
适用场景:
- 适用于缓存数据均匀访问,不需要特定优先级的场景。
优缺点:
✅ 简单高效,减少淘汰策略的计算开销。
❌ 不够智能,可能淘汰热点数据,降低缓存命中率。
4. 基于数据访问频次的淘汰策略
4.1 volatile-lfu(基于访问频率,TTL 限定)
概念:
- 只淘汰设置了 TTL 的 key。
- 访问次数最少的键优先被删除(LFU, Least Frequently Used)。
适用场景:
- 适用于需要根据访问次数保留数据的业务,如热点文章缓存、用户历史记录等。
优缺点:
✅ 能够长期保留高频访问数据,淘汰低频数据。
❌ 如果大部分数据没有 TTL,可能导致 Redis 拒绝写入(类似 noeviction)。
4.2 allkeys-lfu(全局最不常使用淘汰)
概念:
- 无视 TTL,从所有键中优先淘汰访问次数最少的键。
适用场景:
- 适用于热点数据访问有明显差异的情况,如新闻热点推荐、热门产品缓存等。
优缺点:
✅ 能保留长期热点数据,提高缓存命中率。
❌ 短期热点数据可能无法及时替换,导致数据更新滞后。
总结
策略 | 机制 | 适用场景 | 优点 | 缺点 |
---|---|---|---|---|
noeviction | 拒绝写入 | 不能丢数据(消息队列、金融) | 数据安全 | 容易写满导致错误 |
volatile-lru | 仅淘汰 TTL 数据,LRU | 需自动过期,保留热点数据 | 减少缓存击穿 | 仅适用于部分数据有 TTL |
volatile-ttl | 仅淘汰 TTL 数据,剩余寿命短的优先 | 订单缓存、验证码 | 优先清理即将失效的缓存 | 可能误删热点数据 |
allkeys-lru | 全局 LRU 淘汰 | 访问频率高的缓存(推荐系统) | 提高缓存命中率 | 可能误删突然冷却的热点数据 |
allkeys-random | 随机淘汰 | 数据访问均匀的缓存 | 计算开销小 | 可能淘汰重要数据 |
volatile-lfu | 仅淘汰 TTL 数据,访问最少的优先 | 需要根据访问频率保留数据 | 长期热点数据保留 | 仅适用于有 TTL 的 key |
allkeys-lfu | 全局 LFU 淘汰 | 热点明显的数据(新闻、直播) | 缓存命中率高 | 短期热点更新慢 |
如何选择淘汰策略?
1. 数据不能丢失(消息队列、金融)
✅ noeviction(拒绝写入)
2. 仅淘汰过期数据(业务数据自动失效)
✅ volatile-lru(保留热点)
✅ volatile-ttl(优先清理快过期数据)
3. 需要智能保留高频访问数据
✅ allkeys-lru(最近最少使用淘汰)
✅ allkeys-lfu(最少使用淘汰)
4. 访问数据均匀,不关心淘汰顺序
✅ allkeys-random(随机删除)
5. 业务需要权衡 LRU 和 LFU
- 短期热点多,选 LRU
- 长期热点多,选 LFU
结论
- 如果数据有 TTL,且希望优先淘汰冷数据,选 volatile-lru / volatile-lfu。
- 如果所有数据都可以被淘汰,选 allkeys-lru / allkeys-lfu。
- 如果只允许写满后拒绝写入,选 noeviction。
- 如果对淘汰规则无特别要求,选 allkeys-random。
正确选择淘汰策略,可以有效提高缓存命中率,降低数据库压力,保障系统稳定性。
常见缓存问题
在 Redis 中,缓存预热、缓存穿透、缓存雪崩和缓存击穿是常见的缓存问题。下面分别描述它们的概念及解决方案:
1. 缓存预热(Cache Warming)
是什么?
缓存预热是指在系统启动或运行之前,提前将热点数据加载到缓存中,以减少数据库的查询压力,提高系统访问速度。
如何解决?
- 手动加载:在服务启动时,手动将热点数据写入缓存。
- 定时刷新:通过定时任务(如 Spring Task、Quartz 等)定期加载热点数据到缓存。
- 数据变更同步:监听数据库更新(如 MySQL binlog、Redis 订阅发布机制),在数据变化时同步更新缓存。
- 批量加载:使用 Redis 的
pipeline
或mset
命令批量写入缓存,提高加载效率。
2. 缓存穿透(Cache Penetration)
是什么?
缓存穿透指的是大量请求查询不存在的数据,导致每次请求都要查询数据库,缓存完全失效,给数据库带来巨大压力。
如何解决?
- 缓存空值:如果查询的数据不存在,可以将空值(如
null
或{}
)存入缓存,并设置较短的过期时间,避免重复查询数据库。 - 布隆过滤器(Bloom Filter):使用布隆过滤器提前判断某个 key 是否可能存在,如果一定不存在,则直接返回,不查询数据库。
- 参数校验:在请求层对参数进行校验,避免无效请求进入系统。
- 限流与黑名单:对异常请求 IP 进行封禁或限流,避免恶意攻击。
3. 缓存雪崩(Cache Avalanche)
是什么?
缓存雪崩指的是大量缓存同时失效,导致短时间内大量请求直接打到数据库,造成数据库压力激增,甚至宕机。
如何解决?
- 缓存过期时间随机化:为缓存设置不同的过期时间,避免大量缓存同时失效,例如使用
TTL = 基础时间 ± 随机时间
。 - 热点数据提前预加载:在缓存即将过期前,主动刷新缓存,保证热点数据始终可用。
- 双层缓存:使用 Redis + 本地缓存(如 Caffeine、Guava Cache),降低对 Redis 的依赖。
- 流量削峰:
- 限流:使用限流算法(如令牌桶、漏桶)限制访问速率。
- 降级:当数据库压力过大时,返回默认值或降级处理。
4. 缓存击穿(Cache Breakdown)
是什么?
缓存击穿指的是某个热点 key 突然失效,导致大量并发请求直接打到数据库,造成数据库短时间内压力剧增。
如何解决?
- 设置热点数据永不过期:对于热点数据,直接不设置过期时间,而是由业务逻辑主动更新缓存。
- 互斥锁(分布式锁):
- 当缓存失效时,多个请求只允许一个线程查询数据库并更新缓存,其他线程等待缓存更新完成后再读取。
- 具体实现:使用
SETNX
(Redis 分布式锁) 或 Redisson。
- 提前刷新缓存:
- 通过异步线程提前更新即将过期的热点缓存,防止突然失效。
- 例如:使用
Redis + 过期监听
,在 key 即将过期前主动更新缓存。
总结
问题 | 现象 | 解决方案 |
---|---|---|
缓存预热 | 缓存刚启动时,没有数据 | 手动加载、定时刷新、监听数据变更 |
缓存穿透 | 查询的 key 在数据库中不存在,每次都查数据库 | 缓存空值、布隆过滤器、参数校验、黑名单 |
缓存雪崩 | 大量 key 同时失效,数据库压力激增 | 过期时间随机化、双层缓存、限流、降级 |
缓存击穿 | 某个热点 key 失效,大量请求打到数据库 | 热点数据永不过期、分布式锁、提前刷新 |
这四个缓存问题都是分布式系统中必须重点关注的,合理的缓存策略可以有效提升系统的性能和稳定性。