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

本文探讨了数据库与缓存保持一致性的必要性,介绍了常见的解决方法如全量刷缓存、热点数据缓存、先更新数据库后删除缓存等,并针对失效问题提出了延时双删和消息中间件等解决方案。

数据库和缓存的一致性保证是常见的一道面试题。网上有很多博客都已经介绍过该问题,阐述了很多解决办法,包括这些解决办法的优缺点,本人看过很多解决办法,想写点自己的个人体会,用于面试时的回答。

面对数据库与缓存一致性的问题,我们首先应该了解以下几个问题,这些问题能帮助我们更好的理解如何 保持数据库和缓存一的致性 这个问题。

  • 什么是数据库、缓存?
  • 为什么数据库与缓存要保持一致?
  • 数据库和缓存保持一致有哪几种方法?
  • 如果没有保持一致会出现什么问题?
  • 有什么其它方式可以让数据库和缓存保持一致?

一、为什么数据库与缓存要保持一致

1、数据库和缓存是什么

数据库用于存储数据,常用作项目底层存储数据,在项目需要使用数据时会向数据库发送请求,数据库能够快速响应请求,并准确返回数据。常见的数据库有MySQL和Oracle等。

数据库中的数据存储在磁盘,项目获取数据会涉及到 IO操作 。现阶段的项目会频繁获取数据,会涉及大量 IO操作 ,就会产生性能问题。访问内存是计算机内部的一种基本操作,不涉及IO操作。 如果将数据库中的信息存储到内存中,就可以快速访问,并不涉及 IO操作

缓存是用来避免频繁地到数据库或磁盘文件获取数据而建立的一个 快速临时存储器 。缓存通常比数据库或磁盘的容量小,但存取速度非常快。它主要用于存储频繁访问的数据、临时存储耗时的计算结果,以减少对磁盘的I/O操作。

2、为什么数据库与缓存要保持一致

物品数量信息访问缓存得到,用户A购买了物品之后,数据库信息更新,但是并未及时更新缓存,用户B购买相同物品,就会出现问题。缓存和数据库不一致时,对大型项目的数量和金额都会产生重大生产事故。


二、解决的方案

项目会随着业务量的增长,请求量越来越大,如果每次都从DB中读取,那必然会产生性能问题。通常这个阶段就会引入 缓存 来提高读写性能。Redis 是目前主流的缓存中间件,不仅性能高,还支持多种数据类型。


方案1、数据库全量数据刷到缓存中,定时任务更新缓存

数据库的数据,全量刷到缓存中(不设置失效时间),写请求只更新数据库,不更新缓存。通过定时任务将数据库中的数据,同步更新到缓存中。所有请求都全部 命中 缓存,不需要经过数据库,性能非常高。

但是缺点也很明显:全量数据中有部分数据不一定是热点数据,会导致资源浪费。部分数据在一定时间内不一致,对于访问到这部分数据的业务,就会出现问题。


方案2、数据库热点数据刷到缓存中,定时任务更新缓存,缓存设置过期时间

首先要全量数据写入,对所有数据设置失效时间,会逐渐淘汰掉不常访问的数据,即 热点数据

这种方案里,写操作只写数据库,当读请求没有命中缓存时,就从数据库中读,并构建该数据的热点,同时设置一个过期时间。缓存中不经常访问的key,随着时间的增加,逐渐失效,最终缓存中保留的,都是热点数据。优点是缓存的利用率比较高,缺点还是会存在数据不一致的情况。

相比于方案一,能确定的是,想要提高缓存利用率,就要对缓存中的数据设置失效时间。


缓存利用率解决了,就来看看如何确保数据库数据与缓存数据的一致性问题。前面是通过定时任务刷新缓存,数据库数据在一定时间内与缓存数据会出现不一致。想要保证 实时一致 ,那就 不能再使用定时任务刷新缓存 的方案。

当数据发生更新时,不仅要操作更新数据库,还要一并操作缓存。 这就会有以下两种方案。

  • 先更新数据库,再更新缓存
  • 先更新缓存,再更新数据库

方案3、先更新数据库,再更新缓存

这种方案会面临以下几种问题情况:

  • 写多读少情况: 数据库持续写入数据,并更新到缓存中,但是缓存并未使用,会造成资源浪费。
  • 读取脏数据: 并发情况下,A线程写操作更新数据库,B线程读操作读取缓存,缓存未及时更新,会造成数据脏读。
  • 更新操作失败: 写操作写入数据到数据库成功,更新缓存操作失败了,就会发现修改后的数据并未生效,一直到缓存失效后,访问数据库时才能读取到正确数据。

方案4、先更新缓存,再更新数据库

与方案3一样,面临的情况也差不多。每次数据发生变更,都更新缓存,但是缓存中的数据不一定会被使用,导致缓存中存放了很多不常访问的数据,浪费缓存资源。更新数据库步骤出了问题,写到缓存中的值,与数据库就不同。


数据库和缓存都做更新 并不是一个好办法,会导致用户访问缓存时,获取到的数据和数据库不同,那么不妨考虑一下 删除缓存 ,让访问直接到数据库。这样就会有以下两种方案:

  • 先删除缓存,再更新数据库
  • 先更新数据库,再删除缓存

方案5、先删除缓存,再更新数据库

先删除缓存,再更新数据库,对于串行程序来说,读取的数据是没有问题的。但对于并行程序会出现问题。

  • A线程收到任务,修改数据库X的值,由1改为2。
  • 于是,A线程先执行删除缓存,在这个删除的过程中。
  • B线程收到任务,读取数据库中X的值,发现缓存没有X,于是读取数据库,获取到X=1,并将X=1写入缓存中。
  • 这时,A线程删除完缓存,执行写操作,开始加锁,修改数据库。
  • 最终结果:X的数据库中值为2,B线程X的值为1。

能够肯定的是,会出现上述这种情况。

我们不能保证 更新数据库操作百分百执行 ,只要更新数据库操作失败了,缓存删除成功了,就会保证数据不一致。


方案6、先更新数据库,再删除缓存

先更新数据库,再删除缓存,对于串行程序来说,读取的数据是没有问题的。但对于并行程序也会出现问题,是一个比较极端的情况,即:

  • 缓存中的X数据到了失效时间,突然消失,A线程收到任务,读取数据X,会发现缓存没有,读取数据库X=1。
  • B线程同时收到任务,修改数据库X的值,由1改为2。于是修改数据库( 该操作未加锁 ),A线程就是这时候读到的。
  • B线程修改完数据库,X=2,删除缓存。A线程把X=1写入缓存。

能看出,这种情况发生率很小,加个写锁 就能解决。

在讨论第二步失效的情况,数据库已经更新,缓存未能正确更新,随着失效时间的到来,可以保证会一致。

这种方式可以确保 即使缓存中有旧数据,它也会在数据库更新后被替换

总结:应选择【先更新数据库,再删除缓存】方案


三、应对第二步失效的方案

这几种方案都是典型的两步操作,自身没有办法应对第二步的失效。接下来讨论第二步的失效问题。
网上有很多方案,主要是: 延时双删消息队列加订阅binlog

1、延时双删

延时双删策略很简单,顾名思义就可以,双删就是删两次,延时就是第二次等一会再删,结合我们确定好的 先更新数据库,再删除缓存 策略。

指数据库数据写入完毕之后,删除缓存(第一次删除),延迟一段时间,再次删除缓存(第二次删除)。

对于数据库和缓存的数据一致性,延时双删策略起到了重要的作用。它减少了由于网络延迟、服务器故障等原因导致的缓存和数据库数据不一致的可能性。尤其是在分布式系统中,数据更新或删除操作可能需要跨越多个节点或服务,延时双删策略有助于确保数据的一致性。

延时双删策略并不是强一致性的保证 。无论采用何种方案,都无法完全避免Redis等缓存系统中脏数据的存在,只能减轻这个问题。

延时时间 的主要作用是确保在第一次删除缓存后,其他线程或进程有机会读取到数据库中的最新数据,并将其更新到缓存中。

如果延时时间太短,可能会导致在第二次删除缓存之前,又有新的旧数据被写入缓存,从而无法达到保持数据一致性的目的。而如果延时时间太长,又可能会影响到系统的性能和响应时间。需要根据业务情况进行设定。

2、消息中间件

这种方案是先更新数据库,将删除缓存的作为消息投递到消息队列中。队列对消息进行消费,进行删除缓存,如果失败,则会重试。这种方案就是 利用了消息中间件的重试功能

这种方式很可靠,消息队列中的消息,在成功消费之前不会丢失,包括重启,成功消费后才会删除消息,否则就会进行重试

缺点也很明显,引入了消息队列,系统的复杂性提升,可用性降低。同时也会带来各种各样的问题,例如消息丢失等问题。但是中间件都会有应对策略,只是说系统的吞吐量以及性能有所下降。引入消息队列,带来了可以异步重试的好处,但同时需要通过多种机制去保证删除消息不丢失。

消息中间件保证消息不丢失可以去看一下其他博客,会有精确回答。

3、中间件+订阅数据库binlog日志

项目在修改数据库数据时,MySQL就会产生一条 binlog变更日志),可以使用 消息队列 订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。在MySQL中,可以使用 canal中间件订阅binlog

这种方案的好处是:只要写MySQL成功,肯定会产生变更日志,canal 自动把数据库 变更日志投递给下游 消息队列,缺点是再次使用一个中间件。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值