广东省普宁市诗霜厨卫电器制造有限公司

让建站和SEO变得简单

让不懂建站的用户快速建站,让会建站的提高建站效率!

缓存和数据库一致性问题,看这篇就够了

你好,我是 Kaito。

如何保证缓存和数据库一致性,这是一个须生常谭的话题了。

但好多人对这个问题,依旧有好多猜忌:

到底是更新缓存照旧删缓存? 到底选拔先更新数据库,再删除缓存,照旧先删除缓存,再更新数据库? 为什么要引入音书队伍保证一致性? 延伸双删会有什么问题?到底要不要用? ...

这篇著述,咱们就来把这些问题讲澄莹。

这篇著述干货好多,但愿你不错耐性读完。

引入缓存提高性能

咱们从最通俗的场景运转讲起。

若是你的业务处于起步阶段,流量相等小,那无论是读申请照旧写申请,成功操作数据库即可,这时你的架构模子是这样的:

但跟着业务量的增长,你的式样申请量越来越大,这时若是每次都从数据库中读数据,那确定会有性能问题。

这个阶段平凡的做法是,引入「缓存」来提高读性能,架构模子就变成了这样:

当下优秀的缓存中间件,当属 Redis 莫属,它不仅性能相等高,还提供了好多友好的数据类型,不错很好地得志咱们的业务需求。

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

最通俗成功的有狡计是「全量数据刷到缓存中」:

数据库的数据,全量刷入缓存(不建树失效时辰) 写申请只更新数据库,不更新缓存 启动一个定时任务,定时把数据库的数据,更新到缓存中

这个有狡计的优点是,扫数读申请都不错成功「掷中」缓存,不需要再查数据库,性能相等高。

但纰谬也很彰着,有 2 个问题:

缓存欺诈率低:时时时拜访的数据,还一直留在缓存中 数据不一致:因为是「定时」刷新缓存,缓存和数据库存在不一致(取决于定时任务的引申频率)

是以,这种有狡计一般更安妥业务「体量小」,且对数据一致性要求不高的业务场景。

那若是咱们的业务体量很大,若何措置这 2 个问题呢?

缓存欺诈率和一致性问题

先来看第一个问题,如何提高缓存欺诈率?

想要缓存欺诈率「最大化」,咱们很容易猜测的有狡计是,缓存中只保留最近拜访的「热数据」。但具体要若何做呢?

咱们不错这样优化:

写申请依旧只写数据库 读申请先读缓存,若是缓存不存在,则从数据库读取,并重建缓存 同期,写入缓存中的数据,都建树失效时辰

这样一来,缓存中时时时拜访的数据,跟着时辰的推移,都会冉冉「落伍」淘汰掉,最终缓存中保留的,都是往往被拜访的「热数据」,缓存欺诈率得以最大化。

再来看数据一致性问题。

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

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

但数据库善良存都更新,又存在先后问题,那对应的有狡计就有 2 个:

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

哪个有狡计更好呢?

先不研讨并提问题,曩昔情况下,无论谁先谁后,都不错让两者保持一致,但现时咱们需要要点研讨「非常」情况。

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

这 2 种有狡计咱们一个个来分析。

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

若是缓存更新告捷了,但数据库更新失败,那么此时缓存中是最新值,但数据库中是「旧值」。

诚然此时读申请不错掷中缓存,拿到正确的值,关联词,一朝缓存「失效」,就会从数据库中读取到「旧值」,重建缓存亦然这个旧值。

这时用户会发现我方之前修改的数据又「变且归」了,对业务形成影响。

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

若是数据库更新告捷了,但缓存更新失败,那么此时数据库中是最新值,缓存中是「旧值」。

之后的读申请读到的都是旧数据,只好当缓存「失效」后,智力从数据库中获得正确的值。

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

可见,无论谁先谁后,凡是后者发生非常,就会对业务形成影响。那若何措置这个问题呢?

别急,背面我会详备给出对应的措置有狡计。

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

这里咱们还需要要点顺心:并提问题。

并发激发的一致性问题

假定咱们选择「先更新数据库,再更新缓存」的有狡计,况且两步都不错「告捷引申」的前提下,若是存在并发,情况会是若何的呢?

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

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

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

也即是说,A 诚然先于 B 发生,但 B 操作数据库善良存的时辰,却要比 A 的时辰短,引申时序发生「絮聒」,最终这条数据遵守是不合乎预期的。

相同地,选择「先更新缓存,再更新数据库」的有狡计,也会有雷同问题,这里不再胪陈。

除此除外,咱们从「缓存欺诈率」的角度来评估这个有狡计,亦然不太保举的。

这是因为每次数据发生变更,都「无脑」更新缓存,关联词缓存中的数据不一定会被「随即读取」,这就会导致缓存中可能存放了好多不常拜访的数据,奢华缓存资源。

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

由此可见,这种「更新数据库 + 更新缓存」的有狡计,不仅缓存欺诈率不高,还会形成机器性能的奢华。

是以此时咱们需要研讨另外一种有狡计:删除缓存。

删除缓存不错保证一致性吗?

删除缓存对应的有狡计也有 2 种:

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

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

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

这里我不再胪陈具体场景,你不错按照前边的思绪推演一下,就不错看到依旧存在数据不一致的情况。

这里咱们要点来看「并发」问题。

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

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

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

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

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

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

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

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

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

这种情况「表面」来说是可能发生的,但本质果然有可能发生吗?

其实概率「很低」,这是因为它必须得志 3 个条目:

缓存刚好已失效 读申请 + 写申请并发 更新数据库 + 删除缓存的时辰(方法 3-4),要比读数据库 + 写缓存时辰短(方法 2 和 5)

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

因为写数据库一般会先「加锁」,是以写数据库,平凡是要比读数据库的时辰更长的。

这样来看,「先更新数据库 + 再删除缓存」的有狡计,是不错保证数据一致性的。

是以,咱们应该选择这种有狡计,来操作数据库善良存。

好,措置了并提问题,咱们陆续来看前边留传的,第二步引申「失败」导致数据不一致的问题。

如何保证两步都引申告捷?

前边咱们分析到,无论是更新缓存照旧删除缓存,只须第二步发生失败,那么就会导致数据库善良存不一致。

保证第二步告捷引申,即是措置问题的症结。

想一下,要领在引申过程中发生非常,最通俗的措置方针是什么?

谜底是:重试。

是的,其实这里咱们也不错这样做。

无论是先操作缓存,照旧先操作数据库,凡是后者引申失败了,咱们就不错发起重试,尽可能地去做「赔偿」。

那这是不是意味着,只须引申失败,咱们「无脑重试」就不错了呢?

谜底是申辩的。现实情况往往莫得想的这样通俗,失败后立即重试的问题在于:

立即重试很简略率「还会失败」 「重试次数」建树几许才合理? 重试会一直「占用」这个线程资源,无法业绩其它客户端申请

看到了么,诚然咱们想通过重试的阵势措置问题,但这种「同步」重试的有狡计依旧不严谨。

那更好的有狡计应该若何做?

谜底是:异步重试。什么是异步重试?

其实即是把重试申请写到「音书队伍」中,然后由特意的枉然者来重试,直到告捷。

或者更成功的做法,为了幸免第二步引申失败,咱们不错把操作缓存这一步,成功放到音书队伍中,由枉然者来操作缓存。

到这里你可能会问,写音书队伍也有可能会失败啊?而且,引入音书队伍,这又增多了更多的珍摄资本,这样做值得吗?

这个问题很好,但咱们思考这样一个问题:若是在引申失败的线程中一直重试,还没等引申告捷,此时若是式样「重启」了,那此次重试申请也就「丢失」了,那这条数据就一直不一致了。

是以,这里咱们必须把重试或第二步操作放到另一个「业绩」中,这个业绩用「音书队伍」最为合适。这是因为音书队伍的特质,碰无意乎咱们的需求:

音书队伍保证可靠性:写到队伍中的音书,告捷枉然之前不会丢失(重启式样也不驰念) 音书队伍保证音书告捷送达:下流从队伍拉取音书,告捷枉然后才会删除音书,不然还会陆续送达音书给枉然者(合乎咱们重试的场景)

至于写队伍失败和音书队伍的珍摄资本问题:

写队伍失败:操作缓存和写音书队伍,「同期失败」的概率其实是很小的 珍摄资本:咱们式样中一般都会用到音书队伍,珍摄资本并莫得新增好多

是以,引入音书队伍来措置这个问题,是比拟合适的。这时架构模子就变成了这样:

那若是你照实不想在应用中去写音书队伍,是否有更通俗的有狡计,同期又不错保证一致性呢?

有狡计照旧有的,这即是近几年比拟流行的措置有狡计:订阅数据库变更日记,再操作缓存。

具体来讲即是,咱们的业务应用在修改数据时,「只需」修改数据库,无需操作缓存。

那什么时候操作缓存呢?这就和数据库的「变更日记」关系了。

拿 MySQL 例如,当一条数据发生修改时,MySQL 就会产生一条变更日记(Binlog),咱们不错订阅这个日记,拿到具体操作的数据,然后再把柄这条数据,去删除对应的缓存。

订阅变更日记,现时也有了比拟闇练的开源中间件,例如阿里的 canal,使用这种有狡计的优点在于:

无需研讨写音书队伍失败情况:只须写 MySQL 告捷,Binlog 确定会有 自动送达到下流队伍:canal 自动把数据库变更日记「送达」给下流的音书队伍

天然,与此同期,咱们需要干涉元气心灵去珍摄 canal 的高可用和幽静性。

若是你有寄望知悉很无数据库的特质,就会发现其实很无数据库都冉冉运转提供「订阅变更日记」的功能了,信服不远的畴昔,咱们就无谓通过中间件来拉取日记,我方写要领就不错订阅变更日记了,这样不错进一步简化经由。

至此,咱们不错得出论断,想要保证数据库善良存一致性,保举选择「先更新数据库,再删除缓存」有狡计,并相助「音书队伍」或「订阅变更日记」的阵势来做。

主从库延伸和延伸双删问题

到这里,还有 2 个问题,是咱们莫得要点分析过的。

第一个问题,还记起前边讲到的「先删除缓存,再更新数据库」有狡计,导致不一致的场景么?

这里我再把例子拿过来让你温习一下:

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

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

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

第二个问题:是对于「读写分别 + 主从复制延伸」情况下,缓存和数据库一致性的问题。

在「先更新数据库,再删除缓存」有狡计下,「读写分别 + 主从库延伸」其实也会导致不一致:

线程 A 更新主库 X = 2(原值 X = 1) 线程 A 删除缓存 线程 B 查询缓存,莫得掷中,查询「从库」获得旧值(从库 X = 1) 从库「同步」完成(主从库 X = 2) 线程 B 将「旧值」写入缓存(X = 1)

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

看到了么?这 2 个问题的中枢在于:缓存都被回种了「旧值」。

那若何措置这类问题呢?

最灵验的方针即是,把缓存删掉。

关联词,不行立即删,而是需要「延伸删」,这即是业界给出的有狡计:缓存延伸双删计策。

按照延时双删计策,这 2 个问题的措置有狡计是这样的:

措置第一个问题:在线程 A 删除缓存、更新完数据库之后,先「寝息一会」,再「删除」一次缓存。

措置第二个问题:线程 A 不错生成一条「延时音书」,写到音书队伍中,枉然者延时「删除」缓存。

这两个有狡计的地方,都是为了把缓存清掉,这样一来,下次就不错从数据库读取到最新值,写入缓存。

但问题来了,这个「延伸删除」缓存,延伸时辰到底建树要多久呢?

问题1:延伸时辰要大于「主从复制」的延伸时辰 问题2:延伸时辰要大于线程 B 读取数据库 + 写入缓存的时辰

关联词,这个时辰在散播式和高并发场景下,其实是很难评估的。

好多时候,咱们都是凭借教训大要估算这个延伸时辰,例如延伸 1-5s,只可尽可能地裁减不一致的概率。

是以你看,选择这种有狡计,也仅仅尽可能保证一致性汉典,顶点情况下,照旧有可能发生不一致。

是以本质使用中,我照旧提倡你选择「先更新数据库,再删除缓存」的有狡计,同期,要尽可能地保证「主从复制」不要有太大延伸,裁减出问题的概率。

不错做到强一致吗?

看到这里你可能会想,这些有狡计照旧不够无缺,我就想让缓存和数据库「强一致」,到底能不行做到呢?

其实很难。

要想做到强一致,最常见的有狡计是 2PC、3PC、Paxos、Raft 这类一致性左券,但它们的性能往往比拟差,而且这些有狡计也比拟复杂,还要研讨多样容错问题。

违抗,这时咱们换个角度思考一下,咱们引入缓存的地方是什么?

没错,性能。

一朝咱们决定使用缓存,那势必要濒临一致性问题。性能和一致性就像天平的两头,无法做到都得志要求。

而且,就拿咱们前边讲到的有狡计来说,当操作数据库善良存完成之前,只须有其它申请不错进来,都有可能查到「中间情状」的数据。

是以若曲直要追求强一致,那必须要求扫数更新操作完成之前时代,不行有「任何申请」进来。

诚然咱们不错通过加「散播锁」的阵势来罢了,但咱们要付出的代价,很可能会跳动引入缓存带来的性能进步。

是以,既然决定使用缓存,就必须容忍「一致性」问题,咱们只可尽可能地去裁减问题出现的概率。

同期咱们也要暴露,缓存都是有「失效时辰」的,就算在这时代存在短期不一致,咱们依旧有失效时辰来兜底,这样也能达到最终一致。

回想

好了,回想一下这篇著述的要点。

1、想要提高应用的性能,不错引入「缓存」来措置

2、引入缓存后,需要研讨缓存和数据库一致性问题,可选的有狡计有:「更新数据库 + 更新缓存」、「更新数据库 + 删除缓存」

3、更新数据库 + 更新缓存有狡计,在「并发」场景下无法保证缓存和数据一致性,且存在「缓存资源奢华」和「机器性能奢华」的情况发生

4、在更新数据库 + 删除缓存的有狡计中,「先删除缓存,再更新数据库」在「并发」场景下依旧特地据不一致问题,措置有狡计是「延伸双删」,但这个延伸时辰很难评估,是以保举用「先更新数据库,再删除缓存」的有狡计

5、在「先更新数据库,再删除缓存」有狡计下,为了保证两步都告捷引申,需相助「音书队伍」或「订阅变更日记」的有狡计来做,实质是通过「重试」的阵势保证数据一致性

6、在「先更新数据库,再删除缓存」有狡计下,「读写分别 + 主从库延伸」也会导致缓存和数据库不一致,缓解此问题的有狡计是「延伸双删」,凭借教训发送「延伸音书」到队伍中,延伸删除缓存,同期也要甘休主从库延伸,尽可能裁减不一致发生的概率

跋文

本认为这个须生常谭的话题,写起来很好写,没猜测在写的过程中,照旧挖到了好多之前莫得深度思考过的细节。

在这里我也共享 4 点心得给你:

1、性能和一致性不行同期得志,为了性能研讨,平凡会选择「最终一致性」的有狡计

2、摆布缓存和数据库一致性问题,中枢问题有 3 点:缓存欺诈率、并发、缓存 + 数据库一齐告捷问题

3、失败场景下要保证一致性,常见妙技即是「重试」,同步重试会影响模糊量,是以平凡会选择异步重试的有狡计

4、订阅变更日记的思惟,实质是把泰斗数据源(例如 MySQL)当做 leader 副本,让其它异质系统(例如 Redis / Elasticsearch)成为它的 follower 副本,通过同步变更日记的阵势,保证 leader 和 follower 之间保持一致

好多一致性问题,都会选择这些有狡计来措置,但愿我的这些心得对你有所启发。

本文转载自微信公众号「水点与银弹」,不错通过以下二维码顺心。转载本文请关系水点与银弹公众号。

 



上一篇:奚梦瑶为何猷君剃头,配偶互动友爱温馨    下一篇:天津再行印发公积金新政:父母可索要住房公积金撑持子女购房