缓存预热与更新策略:启动时如何预热?更新时如何保证一致性?
大家好,我是你们的老朋友。今天咱们聊聊缓存预热和更新策略。这两个问题,说白了就是「缓存怎么用才不翻车」。我在项目中见过太多人,缓存用得很爽,结果一重启系统,请求全打到数据库上,直接给干趴下了。嗯,今天咱们就把这事彻底聊透。
一、启动时如何预热?
缓存预热,听起来高大上,其实就一句话:在系统启动时,把热点数据提前加载到缓存里。为什么要这么做?你想想看,系统刚启动,缓存里空空如也。这时候来一个用户请求,缓存没命中,就得去查数据库。如果同时来一万个请求,数据库直接扛不住。
核心原则:不要让用户成为缓存的第一批填充者。
我个人习惯的做法是,在应用启动后,用一个后台线程去加载热点数据。比如电商系统,商品详情页的访问量最大。我会在启动时,把最近7天销量前1000的商品信息加载到缓存里。
代码大概长这样:
@Component
public class CachePreheater implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// 启动后异步预热
CompletableFuture.runAsync(() -> {
List<Long> hotProductIds = productService.getHotProductIds(1000);
for (Long id : hotProductIds) {
Product product = productService.getById(id);
cacheService.set("product:" + id, product);
}
});
}
}
这里有个坑,我曾经踩过。预热时如果一次性加载太多数据,会导致缓存服务OOM。所以建议分批次加载,每批100条,中间加个sleep。别急,慢一点总比挂掉好。
小技巧:预热的数据量,建议控制在缓存总容量的60%-70%。留点余量给动态数据。
二、更新时如何保证一致性?
缓存和数据库的数据一致性,是缓存系统里最头疼的问题。没有之一。为什么?因为缓存和数据库是两个独立的存储系统,更新操作很难做到原子性。
常见的更新策略有三种:Cache-Aside、Read/Write Through、Write Behind。咱们一个一个说。
1. Cache-Aside(旁路缓存)
这是最常用的模式。说白了就是:应用自己管理缓存和数据库的读写。
- 读操作:先查缓存,命中则返回;没命中则查数据库,然后写入缓存。
- 写操作:先更新数据库,然后删除缓存(注意,是删除,不是更新)。
为什么写操作要删除缓存而不是更新?我解释一下。假设有两个并发写请求,A先更新数据库为值1,B后更新为值2。如果A和B都去更新缓存,可能A的更新比B慢,导致缓存里存的是旧值1。而删除缓存的话,下次读请求会重新加载,保证读到最新数据。
注意:Cache-Aside模式下,删除缓存可能失败。如果删除失败,缓存里还是旧数据。我建议用重试机制,或者结合消息队列异步重试。
2. Read/Write Through(读写穿透)
这个模式把缓存当成一个代理层。应用只跟缓存打交道,缓存自己负责和数据库同步。
- 读操作:缓存未命中时,缓存自己从数据库加载数据,然后返回给应用。
- 写操作:应用只写缓存,缓存负责把数据同步到数据库。
这种模式的好处是,应用代码更简洁。但坏处也很明显——缓存服务需要支持这种能力。像Redis本身不支持,需要自己封装一层。我在项目中用过Guava Cache的CacheLoader,它支持Read Through,但Write Through得自己实现。
LoadingCache<String, Product> cache = Caffeine.newBuilder()
.build(key -> {
// 缓存未命中时,自动从数据库加载
return productService.getById(Long.parseLong(key));
});
3. Write Behind(写后回写)
这个模式更激进。应用只写缓存,缓存异步地把数据批量写入数据库。
- 优点:写入性能极高,适合写多读少的场景。
- 缺点:如果缓存挂了,数据可能丢失。而且数据一致性是最终一致性,不是强一致性。
我记得有一次做日志收集系统,用的就是Write Behind。每秒几万条日志写入缓存,然后每5秒批量刷一次数据库。性能确实好,但有一次缓存节点宕机,丢了大概10秒的数据。嗯,这个场景可以接受,但金融系统绝对不能用。
选型建议:
| 策略 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|
| Cache-Aside | 最终一致 | 中等 | 通用场景,最推荐 |
| Read/Write Through | 强一致 | 中等 | 需要简化应用代码 |
| Write Behind | 最终一致 | 高 | 写多读少,可接受数据丢失 |
三、核心逻辑流程图
下面这张图,是我自己画的三种策略的核心流程对比。你看一眼就明白了。
四、避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路。
- 缓存穿透:查询一个不存在的key,每次都穿透到数据库。解决方案是缓存空值,或者用布隆过滤器。
- 缓存雪崩:大量缓存同时过期,请求全部打到数据库。解决方案是设置随机过期时间,或者用二级缓存。
- 缓存击穿:一个热点key过期,高并发请求同时打到数据库。解决方案是加互斥锁,或者用永不过期+后台更新。
我的经验:缓存一致性没有银弹。Cache-Aside是最稳妥的选择,配合重试和监控,能解决90%的问题。别为了追求极致性能,用Write Behind去搞金融系统,那是在给自己挖坑。
好了,今天的内容就到这里。缓存预热和更新策略,说白了就是「怎么让缓存干活,又不让缓存惹事」。记住核心原则:预热要早,更新要稳,一致性要盯。
想要更多资料,联系可可