数据库和缓存的一致性保证是常见的一道面试题。网上有很多博客都已经介绍过该问题,阐述了很多解决办法,包括这些解决办法的优缺点,本人看过很多解决办法,想写点自己的个人体会,用于面试时的回答。
文章目录
面对数据库与缓存一致性的问题,我们首先应该了解以下几个问题,这些问题能帮助我们更好的理解如何 保持数据库和缓存一的致性 这个问题。
- 什么是数据库、缓存?
- 为什么数据库与缓存要保持一致?
- 数据库和缓存保持一致有哪几种方法?
- 如果没有保持一致会出现什么问题?
- 有什么其它方式可以让数据库和缓存保持一致?
一、为什么数据库与缓存要保持一致
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 自动把数据库 变更日志投递给下游 消息队列,缺点是再次使用一个中间件。
本文探讨了数据库与缓存保持一致性的必要性,介绍了常见的解决方法如全量刷缓存、热点数据缓存、先更新数据库后删除缓存等,并针对失效问题提出了延时双删和消息中间件等解决方案。
4326

被折叠的 条评论
为什么被折叠?



