当前位置: 首页 > 科技观察

美团两侧:如何保证Redis和MySQL的双写一致性?

时间:2023-03-21 22:59:12 科技观察

前言四月,好朋友去美团面试。他说,有人问他如何保证Redis和MySQL的双写一致性?这个问题其实是在问双写场景下如何保证缓存和数据库的一致性?本文将与您探讨如何回答这个问题。公众号:捡蜗牛的小男孩说的是一致性。一致性是数据的一致性。在分布式系统中,可以理解为多个节点中数据的值是一致的。强一致性:这种级别的一致性最符合用户的直觉。它要求系统写入的内容将是它读出的内容。用户体验是好的,但是实现往往对系统性能有很大的影响。弱一致性:这种一致性级别约束系统不承诺写入成功后立即可以读取写入的值,也不承诺数据多久达到一致性,但会尽量保证一定的时间级别(如第二层)最终数据可以达到一致状态最终一致性:最终一致性是弱一致性的特例,系统会保证在一定时间内数据可以达到一致状态。之所以在这里提出最终一致性,是因为它是弱一致性中备受推崇的一致性模型,也是业界推崇的大型分布式系统数据一致性中的模型。三种经典的缓存模式缓存可以提升性能,缓解数据库压力,但是使用缓存也会造成数据不一致。我们一般如何使用缓存呢?三种经典的缓存使用模式:Cache-AsidePatternRead-Through/Write-throughWrite-behindCache-AsidePatternCache-AsidePattern,即绕过缓存模式,提出来解决缓存与数据库数据不一致的问题尽可能多的问题。Cache-Aside读流程Cache-AsidePattern的读请求流程如下:当一个Cache-Aside读请求被读取时,首先读取缓存。如果缓存命中,则直接返回数据。如果缓存没有命中,它会读取数据库并从数据库中检索数据。放入缓存后,同时返回响应。Cache-Aside写流程Cache-AsidePattern的写请求流程如下:当Cache-Aside写请求更新时,先更新数据库,然后删除缓存。Read-Through/Write-Through(ReadandWrite-Through)在Read/Write-Through模式下,服务器使用缓存作为主要的数据存储。应用程序与数据库缓存的交互是通过抽象缓存层完成的。Read-ThroughRead-Through的简要流程如下:从缓存中读取数据,读取后直接返回。如果无法读取,则从数据库中加载,写入缓存,然后返回响应。这个简短的过程是不是类似于Cache-Aside?其实Read-Through只是多了一层Cache-Provider,其过程如下:更加简洁,同时减少数据源的负载。Write-ThroughWrite-Through模式,当有写请求发生时,缓存抽象层同时完成数据源和缓存数据的更新。流程如下:Write-behind(异步缓存写入)Write-behind和Read-Through/Write-Through有相似之处,CacheProvider负责缓存和数据库读写。它们之间还有一个比较大的区别:Read/Write-Through同步更新缓存和数据,而Write-Behind只更新缓存不直接更新数据库,而是批量异步更新数据库。这种Writebehind进程模式下,缓存和数据库的一致性不强,对一致性要求高的系统慎用。但是适合频繁写入的场景,MySQL的InnoDBBufferPool机制就是采用这种模式。操作缓存时,应该删除缓存还是更新缓存?在日常开发中,我们一般使用Cache-Aside模式。可能有朋友会问,为什么Cache-Aside在写请求的时候会删除缓存,而不是更新缓存呢?Cache-Aside写流程当我们对缓存进行操作时,到底是删除缓存还是更新缓存呢?我们来看一个例子:线程A首先发起写操作。第一步是更新数据库。然后线程B发起写操作。第二步更新数据库。由于网络等原因,线程B首先更新了缓存。线程A更新缓存。.此时缓存保存的是A的数据(旧数据),数据库保存的是B的数据(新数据),数据不一致,出现脏数据。如果删除缓存而不是更新缓存,就不会出现这种脏数据问题。与删除缓存相比,更新缓存有两个缺点:如果你写的缓存值是经过复杂的计算得到的。如果更新缓存的频率很高,就会浪费性能。在数据库写场景多,数据读场景少的情况下,数据往往先更新再读,这样也很浪费性能(其实在写多的场景下使用缓存并不是很划算)是的,哈哈)在双写的情况下,应该先操作数据库还是先操作缓存?在Cache-Aside缓存模式下,有小伙伴还是有疑惑的。为什么写请求的时候先操作数据库呢?为什么不先操作缓存呢?假设有两个请求A和B,请求A执行更新操作,请求B执行查询和读取操作。线程A发起写操作,第一步是delcache,此时线程B发起读操作,cachemiss线程B继续读DB,读出一个旧数据,然后线程B将旧数据置入缓存,线程A写入最新的DBdatasauce有问题,缓存中的数据和数据库不一致。缓存存储旧数据,数据库存储新数据。所以Cache-Aside缓存模式选择先操作数据库,而不是先操作缓存。可能有朋友会问,先操作数据库再操作缓存,会不会造成数据不一致?它们不是原子操作。这是有可能的,但是这种方式下,脏数据一般都是由于缓存删除失败等原因造成的,这种情况出现的概率很低。朋友们可以画出操作流程图先分析一下。接下来我们分析一下在删除缓存失败的情况下如何保证一致性。数据库和缓存数据强一致,有可能吗?事实上,没有办法做到数据库和缓存的绝对一致。可以上锁吗?并发写入时加锁,任何读操作都不写入缓存?缓存和数据库封装CAS乐观锁,通过lua脚本更新缓存?分布式事务,3PC?台积电?其实这是由CAP理论决定的。缓存系统适用的场景是非强一致性场景,属于CAP中的AP。个人感觉在追求绝对一致性的业务场景中引入缓存是不合适的。★CAP理论是指在分布式系统中,Consistency(一致性)、Availability(可用性)、Partitiontolerance(分区容错)不能结合。“但是,通过一些方案优化处理,可以保证弱一致性和最终一致性。三种方案保证数据库和缓存的一致性。可能有朋友会说,先操作数据库没必要。使用缓存延迟双删策略可以保证数据的一致性。什么是延迟双删?延时双删过程先删除缓存,再更新数据库,休眠一段时间(比如1秒),再次删除缓存。这要睡多久?全部1秒?★本休眠时间=读取业务逻辑数据所花费的时间+数百毫秒。为了保证读请求结束,写请求可以删除读请求可能带来的缓存脏数据。“这种方案还不错,只是在sleep期间(比如那1秒),可能有脏数据,一般业务会接受。但是如果第二次删除缓存失败了怎么办?数据缓存和数据库中还是有可能不一致的吧?给Key设置一个自然expire过期时间,让其自动过期如何?那么业务必须接受过期时间内数据不一致的情况?或者有其他更好的解决方案吗?删除缓存重试机制是否是delay双删或者Cache-Aside时,先操作数据库再删除缓存,可能会出现第二步删除缓存失败导致数据不一致的情况,可以使用这个解决方案优化:删除失败多次删除,保证删除缓存成功~所以可以引入删除缓存重试机制,删除缓存重试流程,写请求更新数据库缓存。对于一些原因,删除失败。将删除失败的key放入消息队列,消费消息队列中的消息。keyretrydeletecacheoperationreadbiglog异步删除cacheretrydeletecache机制是可以的,但是会造成很多业务代码入侵。其实也可以这样优化:利用数据库的binlog异步淘汰key。以mysql为例可以使用阿里的canal将binlog日志集合发送到MQ队列,然后通过ACK机制确认并处理这条更新消息,删除缓存,保证数据缓存的一致性