如何保证缓存和数据库的一致性问题?

为什么要引入缓存?

因为数据库是持久化于磁盘中的,而缓存一般是存放于内存中。操作系统对于磁盘的读写性能是只能够达到毫秒级,远不如内存的纳秒级别。

如果使用了缓存来分担数据库的读取操作,尤其是对于写频繁的应用来说,提升是十分的显著的。

在这里插入图片描述

本文的缓存包括中间件(比如Redis)和应用的缓存库(比如Caffiene)

缓存和数据库如何操作?

当我们引入缓存中间件后来进行减轻数据库的读操作时,我们进而带来了一个问题:在写操作时如何保证数据库和缓存的一致性?

我们会从两个维度上入手:

  • 是删除缓存还是更新缓存?
  • 是先操作数据库还是先操作缓存?

删除缓存还是更新缓存?

大部分情况下我们会使用删除缓存的方案,因为考量缓存的性能指标时,一个很关键的因素是缓存利用率。 而对于1000次写请求,我们都进行1000次缓存更新时,那么对于缓存的更新操作是十分浪费的。 因为缓存中的数据并不会被骂上读取到,导致存放了很多不常访问的数据造成资源浪费。

先操作数据库还是先操作缓存

不论是先操作数据库还是先操作缓存,都分为两步,那么就很可能存在第一步成功,第二步失败的情况。

如果是数据库操作成功了,缓存操作失败了,那么缓存依然是旧值,下次读请求时用户看到的依然是旧值。

如果是先缓存操作成功了,后数据库操作失败,那么数据库中的值依然是旧值,就会导致用户的更新丢失

解决方案:重试

好像两个方案都会造成问题,如何解决呢?

最简单的办法就是重试。 不论是哪一种方案,只要第二步发生了失败我们就对其进行重试,理论上就能够保证数据的一致性了。

但是同步重试的并不合理,因为立即重试大概率还是会失败且频繁重试的话会占用系统的资源。

那么我们就会想到异步的解决方案了。

而这个异步的方案我们一般会使用可靠的消息队列来保证其可靠性。

  • 因为消息队列能够保证可靠性:即使应用重启了,消息也不会丢失。
  • 同时消息队列能够保证成功的投递:下游成功消费后才会删除消息,否则还会继续投递给消费者。

解决方案:订阅日志

但是如果是额外引入消息队列,会对于增加我们的运维成本。如果我们不想使用消息队列还有什么解决方案吗?

订阅变更日志

拿MySQL为例,每当一条数据发生修改时,MySQL就会产生一条变更日志,那么我们可以订阅这个binlog,当发生数据的变更时去删除对应的缓存。那么就能够实现数据一致性了。

目前比较成熟的开源中间件就是Canal。我们只需要编写一个Canal Client订阅目标数据库,然后监听对目标数据的更改时,同时操作缓存即可。

主从同步与延迟双删

当我们的数据库使用到了主从同步架构时,并发操作可能会发生如下问题:

  1. 线程 A 更新主库 X = 2(原值 X = 1)
  2. 线程 A 删除缓存
  3. 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)
  4. 从库「同步」完成(主从库 X = 2)
  5. 线程 B 将「旧值」写入缓存(X = 1)

最有效的办法就是把缓存删掉。

但是不能够立即删除,而是需要延迟删除,这就是业界的解决方案:延迟双删策略。

即线程A在删除缓存后,先休眠一会,再删除一次缓存。

而这个延迟时间改设置为多久呢? 在分布式和高并发场景下很难评估,只能够根据不断的尝试尽可能的降低不一致的概率,一般是500ms - 3000ms。

延迟双删还有一种使用场景是先操作缓存再操作数据库时,再次操作缓存。

强一致问题

看到这里你可能会觉得,这些方案依然不够完美,无法做到强一致的情况。 到底能不能做到呢?

其实很难,最常见的方案就是2PC、3PC之类的强一致性协议,而他们的缺陷就是性能较差。

但是我们引入缓存的目的是什么? 性能


所以我们决定使用缓存,就必须容忍一致性的问题,只能够尽可能地降低问题出现的概率。 使用先操作数据库再操作缓存的方案,我们依然有缓存失效时间来兜底达到最终一致性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值