1、缓存与数据库一致性概述:为什么需要一致性、CAP理论在缓存中的应用、常见的缓存策略介绍

大家好,欢迎来到咱们这门课的第一讲。

说实话,缓存与数据库一致性问题,是我在分布式系统里踩过最多的坑之一。我记得刚带团队那会儿,线上一个订单数据对不上,排查了整整两天,最后发现是缓存没及时更新。嗯,从那以后,我对这个问题的敬畏心就上来了。

1.1 为什么需要一致性?

先问个问题:我们为什么要用缓存?

说白了,就是为了扛住高并发,让系统响应更快。但缓存一上,问题就来了——数据库里的数据变了,缓存里的还是老数据。用户读到旧数据,轻则显示异常,重则下单出错、库存对不上。

我在项目中遇到过最典型的一个场景:秒杀活动。用户抢到商品,库存扣了,但缓存没更新。结果下一个用户看到的还是“有货”,点进去却提示“库存不足”。你说用户骂不骂娘?

所以,一致性不是理论问题,是实实在在的线上事故。

核心观点:缓存一致性,本质上是“读”和“写”之间的时间差问题。我们要做的,就是把这个时间差压缩到业务可接受的范围。

1.2 CAP理论在缓存中的应用

聊一致性,绕不开CAP理论。你想想看,分布式系统里,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),三者只能选其二。

缓存系统也不例外。我们通常选择AP(可用性+分区容错性),牺牲强一致性。为什么?因为缓存本身就是用来扛高可用的,你让它为了强一致性去锁住、去等待,那还不如直接查数据库。

场景 选择 说明
秒杀、抢购 AP 允许短暂不一致,保证用户能抢到
支付、转账 CP 必须强一致,宁可失败也不出错
用户信息展示 AP 几秒延迟可以接受

我个人习惯是:先判断业务能不能接受“最终一致性”。如果能,那就用缓存;如果不能,那就别用缓存,直接查库。别为了炫技把系统搞复杂了。

避坑指南:我曾经在一个金融项目里,硬要在缓存里做强一致性,结果引入了分布式锁,性能直接掉了80%。后来老老实实去掉缓存,反而更稳。记住:缓存不是万能的。

1.3 常见的缓存策略介绍

好,既然我们接受了最终一致性,那具体怎么实现?这里介绍几种我常用的策略。

1.3.1 Cache-Aside(旁路缓存)

这是最经典的策略。读的时候先查缓存,没有就查数据库,然后回填缓存。写的时候先更新数据库,再删除缓存。

// 读操作
public String get(String key) {
    String value = cache.get(key);
    if (value == null) {
        value = db.get(key);
        cache.set(key, value);
    }
    return value;
}

// 写操作
public void update(String key, String newValue) {
    db.update(key, newValue);
    cache.delete(key);  // 删除缓存,而不是更新
}

为什么写的时候要删除缓存而不是更新?我解释一下:更新缓存可能产生“脏数据”。比如两个并发写操作,一个写A,一个写B,顺序乱了,缓存里可能存了A写B的数据。删除缓存,下次读的时候自然拉新的,更安全。

注意:Cache-Aside存在一个经典问题——删除缓存后、更新数据库前,如果有读请求进来,会把旧数据回填到缓存。这叫“缓存击穿”。解决办法是加延迟双删,或者用消息队列异步同步。

1.3.2 Read-Through / Write-Through

这种策略把缓存当成一个“代理”。读和写都先经过缓存,由缓存去同步数据库。好处是业务代码简单,坏处是缓存组件本身要承担同步逻辑,复杂度上移。

我在项目中用过一次,后来放弃了。因为一旦缓存挂了,整个读写链路都断了。还是Cache-Aside更灵活,缓存挂了还能直接查库。

1.3.3 Write-Behind(异步回写)

写操作只更新缓存,然后异步批量写入数据库。性能极高,但风险也大——如果缓存宕机,数据就丢了。

说实话,这个策略我一般只在日志、统计这类不敏感数据上用。核心业务数据,我建议慎用。

1.3.4 策略对比

策略 一致性 性能 复杂度 适用场景
Cache-Aside 最终一致 大部分业务场景
Read/Write-Through 强一致 需要缓存代理的场景
Write-Behind 弱一致 极高 日志、统计等非核心数据

我个人最推荐的是Cache-Aside + 延迟双删。简单、可靠、可控。后面几章我会详细讲延迟双删的实现细节,以及怎么用消息队列保证最终一致性。

小结一下:缓存一致性没有银弹。你得根据业务场景选策略,然后接受“最终一致”这个事实。别追求完美,追求可用。

好,这一章就到这里。下一章我们深入聊聊“延迟双删”的具体实现,以及我踩过的那些坑。到时候见。