缓存和数据库一致性问题

文章探讨了在业务增长中引入缓存以提高性能,以及由此带来的缓存利用率和数据一致性问题。介绍了全量刷新、只存储热数据等策略,并分析了先更新缓存和先更新数据库两种方法的优缺点,以及并发情况下可能导致的数据不一致。最后提出删除缓存的方案,并指出先更新数据库再删除缓存能在大多数情况下保证一致性。
摘要由CSDN通过智能技术生成

如何保证缓存和数据库一致性,这是很经典的话题了。

本文将从最简单的场景讲起,循序渐进分析问题。

引入缓存提高性能

在我们业务开始的初期,可能涉及到的流量会很小,这时候我们不管是进行读操作还是写操作,直接操作数据库就可以,此时你的架构模型是这样的:

在这里插入图片描述
但是随着业务量的增长,你的程序处理数据的速度会慢下来,这时候我们通常会选择引入缓存来加快数据处理速度,提高读性能,此时架构模型是这样的:

在这里插入图片描述
当下优秀的缓存中间件,当属 Redis 莫属,它不仅性能非常高,还提供了很多友好的数据类型,可以很好地满足我们的业务需求。

但引入缓存之后,你就会面临一个问题:之前数据只存在数据库中,现在要放到缓存中读取,具体要怎么存呢?

最简单直接的方案是「全量数据刷到缓存中」:

  • 数据库的数据,全量刷入缓存(不设置失效时间)
  • 写请求只更新数据库,不更新缓存
  • 启动一个定时任务,定时把数据库的数据,更新到缓存中

这个方案的优点是,所有读请求都可以直接「命中」缓存,不需要再查数据库,性能非常高。

但缺点也很明显,有 2 个问题:

  1. 缓存利用率低:不经常访问的数据,还一直留在缓存中
  2. 数据不一致:因为是「定时」刷新缓存,缓存和数据库存在不一致(取决于定时任务的执行频率)

所以,这种方案一般更适合业务「体量小」,且对数据一致性要求不高的业务场景。

那如果我们的业务体量很大,怎么解决这 2 个问题呢?

缓存利用率和一致性问题

想要缓存利用率「最大化」,我们很容易想到的方案是,缓存中只保留最近访问的「热数据」。但具体要怎么做呢?

我们可以这样优化:

  • 写请求依旧只写数据库
  • 读请求先读缓存,如果缓存不存在,则从数据库读取,并重建缓存
  • 同时,写入缓存中的数据,都设置失效时间

在这里插入图片描述

这样一来,缓存中不经常访问的数据,随着时间的推移,都会逐渐「过期」淘汰掉,最终缓存中保留的,都是经常被访问的「热数据」,缓存利用率得以最大化。

再来看数据一致性问题。

要想保证缓存和数据库「实时」一致,那就不能再用定时任务刷新缓存了。

所以,当数据发生更新时,我们不仅要操作数据库,还要一并操作缓存。具体操作就是,修改一条数据时,不仅要更新数据库,也要连带缓存一起更新。

但数据库和缓存都更新,又存在先后问题,那对应的方案就有 2 个:

  1. 先更新缓存,后更新数据库
  2. 先更新数据库,后更新缓存

哪个方案更好呢?

先不考虑并发问题,正常情况下,无论谁先谁后,都可以让两者保持一致,但现在我们需要重点考虑「异常」情况。

因为操作分为两步,那么就很有可能存在「第一步成功、第二步失败」的情况发生。

这 2 种方案我们一个个来分析。

先更新缓存,后更新数据库

如果缓存更新成功了,但数据库更新失败,那么此时缓存中是最新值,但数据库中是「旧值」。

虽然此时读请求可以命中缓存,拿到正确的值,但是,一旦缓存「失效」,就会从数据库中读取到「旧值」,重建缓存也是这个旧值。

这时用户会发现自己之前修改的数据又「变回去」了,对业务造成影响。

先更新数据库,后更新缓存

如果数据库更新成功了,但缓存更新失败,那么此时数据库中是最新值,缓存中是「旧值」。

之后的读请求读到的都是旧数据,只有当缓存「失效」后,才能从数据库中得到正确的值。

这时用户会发现,自己刚刚修改了数据,但却看不到变更,一段时间过后,数据才变更过来,对业务也会有影响。

可见,无论谁先谁后,但凡后者发生异常,就会对业务造成影响。那怎么解决这个问题呢?

别急,后面我会详细给出对应的解决方案。

我们继续分析,除了操作失败问题,还有什么场景会影响数据一致性?

这里我们还需要重点关注:并发问题

并发引发的一致性问题

假设我们采用「先更新数据库,再更新缓存」的方案,并且两步都可以「成功执行」的前提下,如果存在并发,情况会是怎样的呢?

有线程 A 和线程 B 两个线程,需要更新「同一条」数据,会发生这样的场景:

  1. 线程 A 更新数据库(X = 1)
  2. 线程 B 更新数据库(X = 2)
  3. 线程 B 更新缓存(X = 2)
  4. 线程 A 更新缓存(X = 1)

最终 X 的值在缓存中是 1,在数据库中是 2,发生不一致。

也就是说,A 虽然先于 B 发生,但 B 操作数据库和缓存的时间,却要比 A 的时间短,执行时序发生「错乱」,最终这条数据结果是不符合预期的。

同样地,采用「先更新缓存,再更新数据库」的方案,也会有类似问题,这里不再详述。

除此之外,我们从「缓存利用率」的角度来评估这个方案,也是不太推荐的。

这是因为每次数据发生变更,都「无脑」更新缓存,但是缓存中的数据不一定会被「马上读取」,这就会导致缓存中可能存放了很多不常访问的数据,浪费缓存资源。

而且很多情况下,写到缓存中的值,并不是与数据库中的值一一对应的,很有可能是先查询数据库,再经过一系列「计算」得出一个值,才把这个值才写到缓存中。

由此可见,这种「更新数据库 + 更新缓存」的方案,不仅缓存利用率不高,还会造成机器性能的浪费。

所以此时我们需要考虑另外一种方案:删除缓存

删除缓存可以保证一致性吗?

删除缓存对应的方案也有 2 种:

  1. 先删除缓存,后更新数据库
  2. 先更新数据库,后删除缓存

经过前面的分析我们已经得知,但凡「第二步」操作失败,都会导致数据不一致。

这里我不再详述具体场景,你可以按照前面的思路推演一下,就可以看到依旧存在数据不一致的情况。

这里我们重点来看「并发」问题。

先删除缓存,后更新数据库

如果有 2 个线程要并发「读写」数据,可能会发生以下场景:

  1. 线程 A 要更新 X = 2(原值 X = 1)
  2. 线程 A 先删除缓存
  3. 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1)
  4. 线程 A 将新值写入数据库(X = 2)
  5. 线程 B 将旧值写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),发生不一致。

可见,先删除缓存,后更新数据库,当发生「读+写」并发时,还是存在数据不一致的情况。

先更新数据库,后删除缓存

依旧是 2 个线程并发「读写」数据:

  1. 缓存中 X 不存在(数据库 X = 1)
  2. 线程 A 读取数据库,得到旧值(X = 1)
  3. 线程 B 更新数据库(X = 2)
  4. 线程 B 删除缓存
  5. 线程 A 将旧值写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),也发生不一致。

这种情况「理论」来说是可能发生的,但实际真的有可能发生吗?

其实概率「很低」,这是因为它必须满足 3 个条件:

  1. 缓存刚好已失效
  2. 读请求 + 写请求并发
  3. 更新数据库 + 删除缓存的时间(步骤 3-4),要比读数据库 + 写缓存时间短(步骤 2 和 5)

仔细想一下,条件 3 发生的概率其实是非常低的。

因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。

这么来看,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。

所以,我们应该采用这种方案,来操作数据库和缓存。

还有更深入的其他的解决方案,碍于文本限制下期再进行详细讲解!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值