缓存预热与更新策略:启动时如何预热?更新时如何保证一致性?

大家好,我是你们的老朋友。今天咱们聊聊缓存预热和更新策略。这两个问题,说白了就是「缓存怎么用才不翻车」。我在项目中见过太多人,缓存用得很爽,结果一重启系统,请求全打到数据库上,直接给干趴下了。嗯,今天咱们就把这事彻底聊透。

一、启动时如何预热?

缓存预热,听起来高大上,其实就一句话:在系统启动时,把热点数据提前加载到缓存里。为什么要这么做?你想想看,系统刚启动,缓存里空空如也。这时候来一个用户请求,缓存没命中,就得去查数据库。如果同时来一万个请求,数据库直接扛不住。

核心原则:不要让用户成为缓存的第一批填充者。

我个人习惯的做法是,在应用启动后,用一个后台线程去加载热点数据。比如电商系统,商品详情页的访问量最大。我会在启动时,把最近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 最终一致 高 写多读少,可接受数据丢失

三、核心逻辑流程图

下面这张图,是我自己画的三种策略的核心流程对比。你看一眼就明白了。

三种缓存更新策略核心流程对比 Cache-Aside 读: 缓存 → DB → 写缓存 写: 更新DB → 删除缓存 一致性: 最终一致 Read/Write Through 读: 缓存自动加载DB 写: 缓存同步写DB 一致性: 强一致 Write Behind 写: 只写缓存 异步批量刷DB 一致性: 最终一致 Cache-Aside 写操作流程 应用 1. 更新DB 数据库 2. 删除缓存 缓存 先更新数据库,再删除缓存。删除失败需重试。

四、避坑指南

最后,分享几个我踩过的坑,希望能帮你少走弯路。

  • 缓存穿透:查询一个不存在的key,每次都穿透到数据库。解决方案是缓存空值,或者用布隆过滤器。
  • 缓存雪崩:大量缓存同时过期,请求全部打到数据库。解决方案是设置随机过期时间,或者用二级缓存。
  • 缓存击穿:一个热点key过期,高并发请求同时打到数据库。解决方案是加互斥锁,或者用永不过期+后台更新。

我的经验:缓存一致性没有银弹。Cache-Aside是最稳妥的选择,配合重试和监控,能解决90%的问题。别为了追求极致性能,用Write Behind去搞金融系统,那是在给自己挖坑。

好了,今天的内容就到这里。缓存预热和更新策略,说白了就是「怎么让缓存干活,又不让缓存惹事」。记住核心原则:预热要早,更新要稳,一致性要盯。

想要更多资料,联系可可