当前位置 百科派 百科知识 正文 下一篇:

如何保证本地缓存、分布式缓存、数据库之间的数据一致性?

在现在的系统架构中,缓存的地位可以说是非常高的。因为在互联网的时代,请求的并发量可能会非常高,但是关系型数据库对于高并发的处理能力并不是非常强,而缓存由于是在内存中处理,并不需要磁盘的IO,所以非常适合于高并发的处理,也就成为了各个系统中必不可少的一部分了。

不过,由此产生的问题也是非常多的,其中一个就是如何保证数据库和缓存之间的数据一致性。

由于数据库的操作和缓存的操作不可能在一个事务中,也就势必会出现数据库写入失败,缓存不能更新,缓存写入失败的补偿机制。具体我们应该怎么做呢?

我们先看一个最常见的读缓存的例子

在读取缓存的方式中,上图这种方式可以说是最为广泛使用的了。读本身是没有什么问题的,但是,写入缓存的方式,就是保证数据一致性的重中之重了。

这里我们不考虑定时刷新缓存的方式,也就是下面这类方式:

写入数据库和写入缓存是独立的,写入数据库操作后,需要等待定时服务执行,执行完成后缓存数据才会刷新。

这种方式会导致数据的不一致时间较长,数据刷新时,不管有没有改变的数据,都会重新加载,效率差。当然,并不是说这种方式就没用,还是有一些场景是可以使用的,例如一些系统配置的缓存,而且,这样做缓存刷新,代码量非常少,也便于维护。

我们今天只考虑双写的数据一致性如何来考虑。由于不同的写入方式,可能带来的结果也就是不同的。通常情况下,我们都有哪些写入数据并刷新缓存的方式呢?

方法一、先更新数据库,在更新缓存

这套方案是最简单的一种缓存双写方案,我们先来看看流程图

使用这种双写的方案,只要在数据成功写入数据库后,刷新缓存就可以了,代码简单,维护也很简单。但是,简单的前提下,带来的问题也是很直接的。

首先,线程数据安全无法保证

例如:我们现在同时有两个请求会操作同一条数据,一个是请求A,一个是请求B。请求A需要先执行,请求B后执行,那么数据库的记录就是请求B执行后的记录。

但是,由于一些网络原因或者其他情况,最终执行的顺序可能就变成了:

请求A Update 数据库 -> 请求B Update 数据库 -> 请求B Update 缓存 -> 请求A Update 缓存。

这样的结果会导致:

1. 数据库和缓存中的数据不一致,从而缓存中的数据就成为了脏数据。

2. 写入操作多于读操作,就会频繁的刷新缓存,但是这些数据根本没有被读过。这样就会浪费服务器的资源。

因此,这种双写方式很难保证数据一致性,不建议使用。

方法二、先删除缓存再更新数据库

由于上述方式存在的问题,那么我们就考虑,能不能先删除缓存,在更新数据库,这样,在更新数据库的前后,由于缓存中没有数据了,请求就会穿透到数据库直接读取数据然后放入缓存,这样,缓存就不会被频繁的刷新了。

于是,我们就设置了一个新的执行顺序:

不过,这样一来,新问题又出现了。有两个请求,一个请求A,一个请求B,请求A去写数据,请求B去读数据。当并发量高的时候,就会出现以下情况:

请求A进行写操作,删除缓存 -> 请求B查询发现缓存不存在 -> 请求B去数据库查询得到旧值 -> 请求B将旧值写入缓存 -> 请求A将新值写入数据库

这是,脏数据又出现了。如果我们没有设置缓存的过期时间,那么在下一次下入数据前,脏数据就会一直的存在。针对这种脏数据出现的情况,我们决定在写入数据后,增加一点延时,再删除一次数据,于是就有了方法三。

方法三、延时双删

使用延时双删的策略,就能够很好的解决之前我们应该并发所引起的数据不一致的情况。那是不是延时双删就完全没有问题呢?不。

我们来假设一个场景,就是我们做了读写分离,那么使用延时双删可能问出现什么情况呢?

请求A进行写操作,删除缓存 -> 请求A将数据写入数据库了 -> 请求B查询缓存发现,缓存没有值 -> 请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值 -> 请求B将旧值写入缓存 -> 数据库完成主从同步,从库变为新值。

糟糕,又出现数据不一致了。

然后在看看性能如何,由于需要延时,如果是同步执行,性能必定很差,所以第二次删除只有做成异步,避免影响性能。那异步执行删除就会出现新问题,如果异步线程执行失败了,那么旧数据就不会被删除,数据不一致又出现了。

不行,我们需要向一个一劳永逸的办法,单纯的双删还是不可靠。

方法四、队列删除缓存

我们在把数据更新到数据库后,把删除缓存的消息加入到队列中,如果队列执行失败,就再次加入到队列执行直到成功为止。

这样,我们就能够有效的保证数据库和缓存的数据一致性了,不管是读写分离还是其他情况,只要队列消息能够保证安全,那么缓存就一定会被刷新。

当然,根据这个方案,我们还可以进一步优化。因为这里我们的缓存刷新时基于业务代码的,也就是说,业务代码和缓存刷新的耦合度很高。有没有办法能够把缓存刷新独立出来,不基于业务代码执行呢?

方法五、binlog订阅删除缓存

为了保证业务代码的独立性,我们可以通过订阅binlog日志的方式来刷新缓存。我们先启动mysql的binlog日志,然后如下图方式设计流程:

通过binlog的订阅,我们就把业务代码和缓存刷新的非业务代码独立开来。代码量小了,也方便维护了。程序员们也不需要去关心什么时候应该刷新缓存,是不是需要刷新缓存。

当然,实战中,我们还有很多不同的业务场景,可能需要的数据一致性同步方案也不同,这里也只算是一个案例。

很高兴能够看到和回答这个问题,作为一个科技爱好者,我每天都在关注科技发展方面的消息,每天收获也蛮多的。

首先,我觉得这是一个非常好的问题,也是很多小白用户困惑之处,下面我将根据自己的经验认真回答这个问题。

分布式类型高速缓存现在是许多分布式应用程序中必不可少的组件,但是如果以分布式格式使用高速缓存,则它可能与高速缓存和双数据库关联。如果您写两次,那么数据序列肯定会出现问题,您该如何解决兼容性问题?

最经典的缓存+数据库模式是“缓存备用模式”

读取高速缓存,不带单词的高速缓存,读取数据库,然后删除数据并将其放入高速缓存中,然后作为响应进行回调。更新数据库,然后删除缓存。

为什么要删除缓存而不更新缓存?

原因是在许多情况下,缓存是复杂点的场景,缓存不只是从数据库输出。例如,您可以先更新一个表字段,然后再更新相应的缓存。为了计算最新的缓存值,有必要从另外两个表中请求数据并执行操作。

有时,更新缓存的成本非常高。这是否意味着每次对数据库进行更改时,都需要更新相应的缓存?也许有这样的场景,但是对于更复杂的缓存脚本,情况不再如此。如果您经常更改缓存,那么它也会经常更新。但是问题在于缓存将不经常可用吗?

如果添加栗子,则包括表字段的缓存将更改20次或在1分钟内更改100次,然后将缓存更新20次和100次,但是每分钟仅读取一次缓存,其中包含有关冷的大量数据。实际上,如果您只是删除缓存,那么在一分钟之内就可以对缓存进行简单的重新计数,并且所花费的费用比缓存要少得多。

实际上,删除缓存而不是更新缓存是一种惰性计算的思想,无论是否使用缓存,都不要每次都进行复杂的计算,而是在必要时重新进行计算。就像moibatis,冬眠一样,每个人都认为自己很懒。应部门的要求,部门带来了一份员工名单,无需说出每个部门的要求,它还包含1000名员工的数据。在80%的情况下,检查该扇区足以访问有关该扇区的信息。首先检查部门,还联系内部的员工,然后仅当您要访问数据库中的人员时,才可以联系1000名员工。

缓存问题和解决方案

问题:首先更改数据库,然后删除缓存。如果缓存删除错误,则将导致以下事实:数据库中的数据将是新的,缓存中的数据将是旧的,并且数据将不一致。

解决方案:首先删除缓存,然后修改数据库。如果数据库更改错误,则表明数据库中有旧数据,并且高速缓存为空,则数据不会彼此不同。由于无法读取缓存,因此请读取数据库中的旧数据并在缓存中进行更新。

分析更复杂的数据不一致问题

更改了数据,首先删除了缓存,然后对数据库进行了更改,直到进行了更改。读取缓存的一个请求,发现缓存为空,转到数据库,在进行更改之前找到了旧数据,并将其放入缓存中。数据修改程序已完成数据库的更新。

一切都消失了,数据库和数据中的缓存不相似…

为什么缓存与脚本结合使用时会产生数十亿的高流密度的问题?

仅当同时读取和写入数据时,才会出现此类问题。实际上,如果您的并行增长非常低,尤其是阅读和分发,则每天的访问量为10,000,那么在极少数情况下,就会出现刚才提到的不一致的情况。但是,问题在于,如果流的日流量为每秒1亿,则如果请求更新数据,则上述数据库+缓存之间可能不匹配。

解决方法如下:

当更新数据时,根据单个数据标识符,将在发送到jvm内部队列后沿该路由执行操作。当读取数据时,如果在缓存中找不到数据,则会根据将发送到jvm内部队列的唯一标识路由,重新读取数据并更新缓存操作。

队列对应于一个工作讨论,每个讨论按顺序执行,然后执行一篇文章。因此,数据修改操作首先删除高速缓存,然后更新数据库,但不更新它。同时,如果读取了读取请求且缓存为空,则可以先将请求更新缓存以发送到队列。此时,队列中的滞后将累积,然后同步将等待缓存更新完成。

这里有一个优化点,一个队列中没有几个请求一起更新缓存,因此您可以过滤是否已经有更新队列中的缓存的请求,您不需要在队列中输入更新请求,只需等到请求更新将完成。

在此队列完成上一个操作的数据库中的更改之后,将执行下一个操作,即缓存将被更新。在这种情况下,将读取数据库中的最后一个值并将其输入到高速缓存中。

如果该请求仍在可及范围内,并且事实证明该请求可用,则立即返回;如果等待时间超过一定时间,则这次将直接从数据库中读取当前的旧值。

在高度一致的情况下,该解决方案需要注意:

1.Ready请求被长时间阻止

由于读取请求非常轻巧且异步,因此必须注意读取延迟,并且每个读取请求都应在等待时间内返回。

该决定主要是由于很可能会经常更新数据,这导致以下事实:队列中积累了大量更新操作,然后在读取请求中发生了许多中断,最终导致大量查询直接进入数据库。有必要通过进行一系列模拟和实际测试来检查数据更新的频率。

此外,由于在其中一个队列中可能存在积压的多个位置更新数据,因此有必要考虑到您自己的操作来检查它们,因此您可能需要部署多个服务,每个服务都将分发部分数据更新操作。如果在存储队列中您确实从修改库存中挤出了100种商品,则每次库存更改需要10毫秒才能完成时,那么读取最后一种商品的请求可能会等待10*100=1000ms=1s来接收数据,这会导致较长的读取延迟要求。

必须根据实际操作系统进行几次压力测试,并对网络环境进行建模,以查看内存队列有多繁忙可以产生更新,这可能导致对应于读取请求的最新更新,如果读取请求返回200ms,则挂起要花多长时间如果您甚至计算出最紧张的时间,即10次更新的滞后,最多200ms,那么这是可能的。

如果大量更新可以在内存队列中累积,那么您需要添加设备,以使部署在计算机上的服务的每个实例处理的数据更少,那么队列中累积的更新数量将减少。

实际上,根据先前项目的经验,数据记录的频率通常很低,因此在实践中,通常,在队列中更新的滞后应该微不足道。与该项目有关读取和读取缓存的体系结构的情况一样,写入请求通常非常少,每秒QPS可以达到数百。

1、实际流利度计算

如果在一秒钟内执行500次写入操作,如果除以5个计时器,每200ms100次操作以及该存储队列中的20个操作,则每个存储队列可能会延迟5次写入操作。通常,在每次记录性能测试之后,大约需要20ms,然后读取每个内存队列中的数据的请求也将挂起一会儿,可以返回200ms。

经过简单的测量,我们知道支持一台机器成百上千个QPS并不是问题,如果将QPS记录增加10倍,则该机器将扩展10倍,每台机器20个队列。

2、读取请求过多

还必须在此处进行压力测试,以确保在遇到上述情况时,存在服务意外的大量读取请求被延迟数十毫秒的风险,以使服务了解如何进行服务不可用,以及需要多少辆汽车才能承受最大峰值。

但是,由于并非同时更新所有数据,因此高速缓存不会同时过期,因此,每次高速缓存中可能会丢失多个数据时,此数据将成为读取请求,并且它们的数量不是应该特别大。

3、多服务路由请求部署示例

也许此服务包含几个示例,所以有必要确保通过Nginx服务器将数据更新操作的执行以及缓存更新请求的执行定向到同一服务目录。

例如,请求读取和写入同一产品,到同一机器的整个路径。您可以根据所请求的参数之一沿着哈希路由前往服务办公室,也可以使用Nginx哈希路由,依此类推。

4、热货路线问题,导致要求

如果读取和写入产品的顺序特别高,则与在同一台计算机上相同的队列中的所有内容都可能对同一台计算机造成过大的压力。这意味着,由于仅在更新产品数据时才清除缓存,然后它们导致读写,实际上是看业务系统,如果刷新率不太高,则此问题的后果不会特别明显很好,但是,当然,某些汽车上可能会有很大的负载。

以上便是我的一些见解和回答,可能不能如您所愿,但我真心希望能够对您有所帮助!不清楚的地方您还可以关注我的头条号“每日精彩科技”我将竭尽所知帮助您!

码字不易,感觉写的还行的话,还请点个赞哦!

如何保证本地缓存、分布式缓存、数据库之间的数据一致性?由于涉及到各实例之间的不一致性问题,本地缓存很少用到,随后在结束时提一下,这里重点介绍分布式缓存和数据库之间的一致性。由于受个人项目经历所限,遇到的业务架构较为简单,所能考虑到的业务场景可能不太完善,希望读者辩证的看待相关的论述。

我们一般的业务中,用到的往往是数据库做存储使用,使用使用缓存加速,缓存在一定时间(5分钟或1天)失效后,再查询缓存,然后将数据库查询结果更新到缓存中。即最经典的缓存+数据库读写的模式。

读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。

更新的时候,先更新数据库,然后再删除缓存。

再说说最初级的缓存不一致问题及解决方案,即:先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,出现数据不一致。解决思路:先删除缓存,再修改数据库。如果数据库修改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中。

比较复杂的数据不一致问题分析,即:数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了!只有在对一个数据在高并发(如每天上亿的流量,每秒并发读是万级的QPS,每秒有数据更新的请求)的进行读写的时候,才可能会出现这种问题。解决方案如下:更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。即将该读请求进行非常轻度的异步化,当发现缓存中无数据,数据库中的数据正在变更时,进行异步请求排队阻塞。

该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。同时也需要关注读请求并发量过高、多服务实例部署的请求路由等问题,确保读请求并发量过高时能在毫秒级返回和同一缓存数据路由到同一台机器上。热点 key 的路由问题可能导致请求倾斜也需要加以关注。

而对于本地缓存数据一致性解决办法,我所接触到的业务中,万级QPS的配置信息读请求引起的缓存击穿问题,最后通过本地缓存该配置信息30s来解决的。该配置信息产品侧接受30s内的配置不一致情况,终端用户几乎无感知。而查找了一些资料,本地缓存数据一致性解决办法有以下几个思路

1、订阅数据变更消息,将数据变更更新回本地缓存;

2、如果订阅变更消息成本高,则允许短暂的数据不一致(在最终的写服务里,取分布式缓存里的值进行校验,保证最终一致性),设置较短的更新时间(并发读取数越高,localcache收益越大),再去主动更新本地缓存;

3、如果是秒杀系统,提前推送数据到应用本地缓存,之后修改nginx的一致性哈希为轮询,均匀应用访问压力;

4、建立热点发现系统,及时推送热点数据到应用本地。

作者:夕阳雨晴,欢迎关注我的头条号:偶尔美文,主流Java,为你讲述不一样的码农生活。

你也说了是缓存。缓存一般不关注一致性,只关注命中率。

为什么?因为缓存是解决性能和可用性问题的,而解决问题的方式就是拿空间换时间。 所以就一定会对一致性有让步,详细论证参考CAP理论。

那么一定要问,缓存不一致怎么办?不是出大问题了?别着急,没关系。因为缓存场景是有限制的:

1 不要求强一致的,一般来说最终一致即可。(下文有解释)

2 符合命中要求的,非命中通过Miss兜底解决的。说人话就是缓存有的部分肯定一致,不一致就不写入缓存。丢了也不要紧。说白了还是没实现全部强一致。

再说一致性。

简单说,一致性分强一致,弱一致,最终一致三种。

强一致不提了,你说的三层产品根本不是一套,不适合这个情况,不可能实现。

弱一致,也不提了,因为基本上绝大多数满足要求。是你说的三层的正常情况。

最终一致,是这缓存建设的一性能指标,因为最终一致变相影响缓存的基本指标—命中率。这也是缓存和一致性的唯一关联了。

缓存的一致性本来就是一个很复杂的问题,在保证数据安全的前提的下,分解一下的我的做法,授人以鱼,不如授人以渔。这里不讨论什么理论,直接上简单粗暴、立竿见影的处方。以机械设计的思维来解决IT问题。

  1. 缓存存在的意义,“缓存”一词在传统行业中早就有存在,比如机械设计中的安全系数,会根据负荷,材料强度等设置一个区间值,超过区间值就容易出安全事故。在软件项目领域,通常制定项目计划后,会留出20%余量,这是最基础的“缓存”概念,其本质意义是保证系统的高效可用性,尤其在高负荷,大流量情况下。

  2. 缓存存在的问题, 一般情况下适用于“读多写小”的场景,给存储层数据库提供一个安全屏障。命中率低导致大量资源浪费,系统网络通信故障导致缓存层失效,规划设计不合理导致缓存层Down机。

  3. 简单粗暴的解决方法,根据目的、场景、用途不同,设置多级缓存即分级处理。这个原理取自CPU的一级、二级、三级缓存,假如一级缓存命中率为90%,二级缓存命中率为剩下的10%中的90%,三级缓存以此类推。同时必要时配合队列任务、持久化等,可以有效得解决大多数问题。

总之,不同的应用场景,采用不同的解决方法,技术来源于生活,以传统行业的思维来解决IT问题,机械科班出身的IT人共勉。

不可能一致的,如果你写入数据库成功后,还没来得及筏消息到队列之前机器断电了呢?这个问题的根本原因是分布式没有ACID事务。写数据库,写缓存,两者做不到在一个ACID事务里。只有一个办法,就是更新数据库和要删除缓存的消息先在一个数据库本地的ACID事务中完成。然后再把消息从数据库拿出来发送倒队列,如果发送失败就自动重试直到成功为止,然后消息消费者保证堆消息最少消费一次之后再ack。消费完消息之后,把这个消息从数据库删除即可

你不觉得为了强行保证一致性大幅牺牲效率是一件很蠢的事情吗?大部分场景下保证最终一致性即可,迫不得已可以考虑加一把分布式读写锁

其实it从业者不在乎里面的细节,而在于可用的解决方案。

本地缓存的一致性在生产中是两种。一种是服务内变量实时watch etcd做到一致,这类缓存适合应用配置O(a),而不能存放用户进度O(n),内部的raft算法相当拉低qps。

第二种就是时效必须短,10秒到30内不能再多。这种短缓存就可以存临时进度,是浮在redis上面的本地化的二级缓存。这类缓存因为app节点太多,无法强一致,而是每10-30秒后会去redis那达到同步。

分布式缓存,低qps业务直接挂载单点redis,大qps就挂redis集群,cluster那种,分布式一致靠key能hash到同一个slot。

本地缓存加etcd的watch机制,也是可以作为分布式缓存的解决方案的,不过仅限于配置信息,不能缓存进度。

数据库的一致性暂时大多数还是分库分表,通过物理分区和pg上挂一个代理来hash。集群的数据方案大部分是闭源的,这里想分享的人可以在楼里追加。

以上都是中间件自身的一致性。除此之外,才是中间件之间的一致性。

常规的redis和db一致性,一般是通过改库删缓存。

这里直接打死了双写,因为先写完数据库的线程不一定会先写完redis,因为网络,cpu调度等因素,很容易出现数据库后改的东西,在缓存时被先写的覆盖掉了。

同理打死先删缓存再改数据库,因为你删完缓存时,你无法确保在改完数据库前,已经删掉的缓存会不会因为第二次读取又载回缓存了,要知道一个数据库在事务提交前,依旧是可以幻读的。至于改完数据库之后再删一次缓存,其实这个第一个删除就多余了。

再上层一点,就是增加用户锁以及操作单次幂等。如果你很清楚你要保持一致的,是一个用户的某条数据,那么你就可以利用分布式锁,最常用的就是redis的setexnx机制来实现的。

以上是从中间件自身和他们之间的特性总结出来的方案。

在业务上,其实还要就场景来制定不同的方案。比如对数据串行化,最常见的就是redis的原理,以及golang的channel,以及只留写入的唯一入口,队列的单一消费组。

加锁可以保证严格一致,但对可用性影响太大,一般不考虑。剩下你可以根据具体的业务场景容许一定的不一致存在。关键是你要清楚你的方案都会在哪些情况下出现不一致,影响在哪儿。

举个例子,假如你的方案是先更新数据库后清除缓存,让我们分析一下发生什么事会导致不一致?1.两个线程同时读了数据,A线程把数据更新了,B线程拿着的就是过期数据了。2.A线程更新了数据库,还没来得及删除缓存,这时候B线程把数据读走了,B读到的就是过期数据。3.A线程更新了数据库也删除了缓存,但是这个数据库事务还没提交,这时候B线程读到的可能还是旧数据。

我们在看看这些不一致会有什么样的影响:1和2,多线程情况下读到过期数据是正常的,过一会儿刷新一下就会取新数据了,这时候你要解决的问题是不能把手里的数据当最新的用,你要永远假设你手里的数据可能是过期的,比如秒杀,你不能查到了就认为抢到了,这样会超卖的,一般用乐观锁可以解决这个场景中的问题。3就比1和2问题严重了,关键点是B读到的是旧数据并且还会把它缓存起来,由于A已经执行完清缓存的动作了,导致这个失效缓存数据可能会在缓存里呆很久。这种情况你可以用读写锁或者给数据设个过期时间,看具体业务需求。

当然你也可以换成别的方案,比如为了解决上面3的问题,你改成事务后清缓存,3的问题解决了又会带来新的问题,比如你事务刚完成进程就崩了,分布式缓存里的数据就没机会清理了,旧数据同样可能会在缓存了呆很久。

总之,这个问题没有一个普遍适用的解决方案,你要根据你的实际场景选择,关键是你要知道会有哪些不一致的情形出现,你的程序会受怎样的影响,然后才是怎么处理。

不可能,只能最终一致

搞事情!那些不能说的秘密都在这里   关注公众号:百科派  

         

本文来自网络,不代表百科派立场,转载请注明出处:http://www.xinglongcang.com/1078.html

作者: admin

发表评论

您的电子邮箱地址不会被公开。

联系我们

联系我们

0898-88881688

在线咨询: QQ交谈

邮箱: email@wangzhan.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部