缓存更新案例

wen java案例 3

本文目录导读:

缓存更新案例

  1. 核心难点:并发写与缓存一致性问题
  2. 四大主流缓存更新策略
  3. 实战中的权衡与优化
  4. 总结:如何选择?

这是一个非常经典且重要的后端开发问题,缓存更新的目标是保证数据库(DB)和缓存(如Redis)中的数据最终一致,同时兼顾高并发下的性能

下面我来为你详细梳理常见的缓存更新案例,包括其策略、优缺点和适用场景。

核心难点:并发写与缓存一致性问题

假设我们有一个读取流程:

  1. 读请求,先查缓存。
  2. 缓存未命中,查数据库。
  3. 将数据库结果写入缓存。
  4. 返回数据。

问题发生在写操作时:当有并发写请求(更新DB)和读请求(更新缓存)同时发生时,很容易出现数据不一致。


四大主流缓存更新策略

Cache Aside Pattern(旁路缓存)—— 最常用、最推荐

这是业界最标准的模式,具体做法是:

  • 读: 先读缓存,命中则直接返回;未命中则读数据库,然后设置缓存,最后返回。
  • 写: 先更新数据库,然后删除缓存

为什么是“先更新DB,再删除缓存”而不是“先更新缓存”或“先删除缓存”?

原因1:避免并发读写导致的脏数据

  • 场景: 线程A发起写操作,线程B发起读操作。
  • 错误做法(先删缓存,再更新DB):
    1. A删除缓存。
    2. B读缓存未命中,从DB读到旧数据。
    3. B将旧数据写入缓存。
    4. A将新数据写入DB。
    • 结果: 缓存中是旧数据(脏数据),DB是新数据。
  • 正确做法(先更新DB,再删缓存):
    1. A更新DB(新数据)。
    2. B读缓存未命中,从DB读到新数据(或旧数据,取决于隔离级别,但在大多数RC级别下读到新数据概率大)。
    3. B将数据写入缓存。
    4. A删除缓存。
    • 结果: 最终缓存会被删除,下一次读请求会重新从DB加载新数据,保证最终一致性。

原因2:避免删除操作本身失败

  • 如果先删除缓存,然后更新DB失败,缓存已删除,DB数据未变,下次读请求会查到旧数据写入缓存,导致缓存和DB不一致(缓存是旧数据,DB也是旧数据,但业务上可能期望新的)。
  • 如果先更新DB,再删除缓存,即使删除缓存失败,我们还可以通过重试机制(如消息队列、定时任务)来补偿删除操作。

优点: 简单、高效、对并发读友好。 缺点: 短暂的不一致窗口(删除缓存成功前,可能有读请求读到旧缓存),在高并发下,这个窗口非常小,通常可以接受。

Read Through / Write Through(穿透缓存)

应用层不直接与数据库交互,而是通过缓存层(如提供一个缓存抽象层,Caffeine + Redis 的组合)来读写数据。

  • Read Through: 缓存层负责读取数据库,如果缓存未命中,缓存自身会去加载数据库并填充,对应用透明。
  • Write Through: 写请求直接写入缓存,缓存层负责同步写入数据库(同步阻塞)。
    • 优点: 一致性高(写操作必须同时成功或失败)。
    • 缺点: 写性能差(因为要同步写DB),不适合写密集型场景。

Write Behind Caching(异步写回)

数据只写缓存,缓存异步批量写入数据库。

  • 优点: 写性能极高(只写内存),适合写入量巨大、对数据一致性要求不严格的场景(如日志、点赞数、PV/UV统计)。
  • 缺点: 数据有丢失风险(如果缓存宕机),一致性窗口较长,业务需要能容忍不一致。

先更新缓存,再更新数据库(不推荐)

  • 严重缺陷: 如果缓存更新成功,数据库更新失败,则数据永久不一致(缓存有脏数据),几乎只有特殊业务(如并发控制要求极强且必须立即失效的场景,但通常用分布式锁替代)才会使用。

实战中的权衡与优化

在实际业务中,你通常会遇到以下问题,需要做出权衡:

问题1:删除缓存失败怎么办?

方案: 引入重试机制

  1. 设计模式: 在删除操作失败时,将任务(key + 操作类型)写入消息队列(如Kafka、RabbitMQ)。
  2. 异步消费者: 一个异步的消费者监听队列,不断尝试删除该缓存。
  3. 兜底: 可以设置重试次数上限或者延迟重试,确保最终删除成功。

代码示例(伪代码):

public void updateData(Long id, Data data) {
    // 1. 更新数据库
    database.update(id, data);
    // 2. 尝试删除缓存
    try {
        redisCache.delete("data:" + id);
    } catch (Exception e) {
        // 3. 如果删除失败,发送消息到MQ,由MQ消费者重试
        mq.send(new CacheDeleteMessage("data:" + id));
    }
}

问题2:高并发下短时间内不一致怎么办?

方案: 延迟双删

  • 做法: 在“先更新DB,再删缓存”的基础上,延迟一段时间(比如1秒)后再删一次缓存。
  • 原理: 这个延迟时间要大于“读请求从DB读数据到写入缓存”的时间,这样即使有并发读请求在此期间写入了旧缓存,延迟删除也会把它清掉。
  • 缺点: 增加了实现复杂度,且这个“足够长的时间”很难精确估算。

问题3:是否需要加锁?

  • 不需要: 对于绝大多数业务场景,删除缓存策略 + 重试机制已经足够,因为不一致窗口极短,且最终一致。
  • 需要: 如果业务要求“强一致性”(如银行账户余额、秒杀库存),那么应该放弃使用缓存,直接访问数据库,并配合数据库的乐观锁或悲观锁,或者使用分布式锁来对某个Key的读写进行串行化。

如何选择?

业务场景 推荐策略 理由
读多写少,对一致性要求中等 Cache Aside(先更新DB,再删缓存) 简单、高效,不一致窗口可控。
读写比例接近,对一致性要求高 Read Through / Write Through (或 直接读DB) 一致性高,但写性能下降。
大量写操作,偶尔读,允许最终一致 Write Behind (异步写回) 写性能极佳,但有一定数据丢失风险。
高并发,对延迟敏感 在Cache Aside基础上,采用延迟双删 最大程度降低不一致窗口。

最终建议:

在大多数业务系统中,Cache Aside(先更新DB,再删缓存)+ 删除失败的重试机制 是最通用、最稳妥的选择,它平衡了性能、一致性和实现复杂度。

抱歉,评论功能暂时关闭!