社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  DATABASE

如何保证 MySQL 和 Redis 的数据一致性?10 张图带你搞定!

数据分析与开发 • 3 年前 • 365 次点击  

导语 | 本文的主要思路是首先带大家认识了解MySQL和Redis的数据一致性情况,然后进行反推不一致的情况,从而进行探究单线程中的不一致的情况。同时探究多线程中的不一致的情况,拟定数据一致性策略。


一、什么是数据的一致性


“数据一致”一般指的是:缓存中有数据,缓存的数据值=数据库中的值。但根据缓存中是有数据为依据,则“一致”可以包含两种情况:


  • 缓存中有数据,缓存的数据值=数据库中的值


  • 缓存中本没有数据,数据库中的值=最新值(有请求查询数据库时,会将数据写入缓存,则变为上面的“一致”状态)


“数据不一致”:缓存的数据值≠数据库中的值;缓存或者数据库中存在旧值,导致其他线程读到旧数据。



二、数据不一致性情况及应对策略


根据是否接收写请求,可以把缓存分成读写缓存和只读缓存。


只读缓存:只在缓存进行数据查找,即使用“更新数据库+删除缓存”策略。


读写缓存:需要在缓存中对数据进行增删改查,即使用“更新数据库+更新缓存”策略。

 

(一)针对只读缓存(更新数据库+删除缓存)


只读缓存:新增数据时,直接写入数据库;更新(修改/删除)数据时,先删除缓存。后续访问这些增删改的数据时,会发生缓存缺失,进而查询数据库,更新缓存。


  • 新增数据时 ,写入数据库;访问数据时,缓存缺失,查数据库,更新缓存(始终是处于“数据一致”的状态,不会发生数据不一致性问题)



  • 更新(修改/删除)数据时,会有个时序问题:更新数据库与删除缓存的顺序(这个过程会发生数据不一致性问题)


在更新数据的过程中,可能会有如下问题:


  • 无并发请求下,其中一个操作失败的情况。


  • 并发请求下,其他线程可能会读到旧值


因此,要想达到数据一致性,需要保证两点:


  • 无并发请求下,保证A和B步骤都能成功执行。


  • 并发请求下,在A和B步骤的间隔中,避免或消除其他线程的影响。


接下来,我们针对有/无并发场景,进行分析并使用不同的策略。



  • 无并发情况


无并发请求下,在更新数据库和删除缓存值的过程中,因为操作被拆分成两步,那么就很有可能存在“步骤1成功,步骤2失败” 的情况发生(由于单线程中步骤1和步骤2是串行执行的,不太可能会发生 “步骤2成功,步骤1失败” 的情况)。


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



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




解决策略:


a.消息队列+异步重试


无论使用哪一种执行时序,可以在执行步骤1时,将步骤2的请求写入消息队列,当步骤2失败时,就可以使用重试策略,对失败操作进行“补偿”。



具体步骤如下:


  • 把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka消息队列)


  • 当删除缓存值或者是更新数据库值成功时,把这些值从消息队列中去除,以免重复操作。


  • 当删除缓存值或者是更新数据库值失败时,执行失败策略,重试服务从消息队列中重新读取这些值,然后再次进行删除或更新。


  • 删除或者更新失败时,需要再次进行重试,重试超过的一定次数。向业务层发送报错信息。


b.订阅Binlog变更日志


  • 创建更新缓存服务,接收数据变更的MQ消息,然后消费消息,更新/删除Redis中的缓存数据。


  • 使用Binlog实时更新/删除Redis缓存。利用Canal,即将负责更新缓存的服务伪装成一个MySQL的从节点,从MySQL接收Binlog,解析Binlog之后,得到实时的数据变更信息,然后根据变更信息去更新/删除Redis缓存。


  • MQ+Canal策略,将Canal Server接收到的Binlog数据直接投递到MQ进行解耦,使用MQ异步消费Binlog日志,以此进行数据同步。


不管用MQ/Canal或者MQ+Canal的策略来异步更新缓存,对整个更新服务的数据可靠性和实时性要求都比较高,如果产生数据丢失或者更新延时情况,会造成MySQL和Redis中的数据不一致。因此,使用这种策略时,需要考虑出现不同步问题时的降级或补偿方案。



  • 高并发情况


使用以上策略后,可以保证在单线程/无并发场景下的数据一致性。但是,在高并发场景下,由于数据库层面的读写并发,会引发的数据库与缓存数据不一致的问题(本质是后发生的读请求先返回了)


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


假设线程A删除缓存值后,由于网络延迟等原因导致未及更新数据库,而此时,线程B开始读取数据时会发现缓存缺失,进而去查询数据库。而当线程B从数据库读取完数据、更新了缓存后,线程A才开始更新数据库,此时,会导致缓存中的数据是旧值,而数据库中的是最新值,产生“数据不一致”。其本质就是,本应后发生的“B线程-读请求”先于“A线程-写请求”执行并返回了。



或者



解决策略:


设置缓存过期时间+延时双删


通过设置缓存过期时间,若发生上述淘汰缓存失败的情况,则在缓存过期后,读请求仍然可以从DB中读取最新数据并更新缓存,可减小数据不一致的影响范围。虽然在一定时间范围内数据有差异,但可以保证数据的最终一致性。


此外,还可以通过延时双删进行保障:在线程A更新完数据库值以后,让它先sleep一小段时间,确保线程B能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程A再进行删除。后续其它线程读取数据时,发现缓存缺失,会从数据库中读取最新值。


redis.delKey(X)db.update(X)Thread.sleep(N)redis.delKey(X)


sleep时间:在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,以此为基础来进行估算。



注意:如果难以接受sleep这种写法,可以使用延时队列进行替代。


先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,也就是缓存穿透的问题。针对缓存穿透问题,可以用缓存空结果、布隆过滤器进行解决。


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


如果线程A更新了数据库中的值,但还没来得及删除缓存值,线程B就开始读取数据了,那么此时,线程B查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。其本质也是,本应后发生的“B线程-读请求”先于“A线程-删除缓存”执行并返回了。



或者,在“先更新数据库,再删除缓存”方案下,“读写分离+主从库延迟”也会导致不一致:



解决方案:


a.延迟消息


凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率。


b.订阅binlog,异步删除


通过数据库的binlog来异步淘汰key,利用工具(canal)将binlog日志采集发送到MQ中,然后通过ACK机制确认处理删除缓存。


c. 删除消息写入数据库


通过比对数据库中的数据,进行删除确认 先更新数据库再删除缓存,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,也就是缓存穿透的问题。针对缓存穿透问题,可以用缓存空结果、布隆过滤器进行解决。


d.加锁


更新数据时,加写锁;查询数据时,加读锁。



建议:


优先使用“先更新数据库再删除缓存”的执行时序,原因主要有两个:


  • 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力。


  • 业务应用中读取数据库和写缓存的时间有时不好估算,进而导致延迟双删中的sleep时间不好设置。



(二)针对读写缓存(更新数据库+更新缓存)


读写缓存:增删改在缓存中进行,并采取相应的回写策略,同步数据到数据库中。


同步直写:使用事务,保证缓存和数据更新的原子性,并进行失败重试(如果Redis本身出现故障,会降低服务的性能和可用性)


异步回写:写缓存时不同步写数据库,等到数据从缓存中淘汰时,再写回数据库(没写回数据库前,缓存发生故障,会造成数据丢失)

该策略在秒杀场中有见到过,业务层直接对缓存中的秒杀商品库存信息进行操作,一段时间后再回写数据库。


一致性:同步直写>异步回写,因此,对于读写缓存,要保持数据强一致性的主要思路是:利用同步直写,同步直写也存在两个操作的时序问题:更新数据库和更新缓存。


  • 无并发情况




  • 高并发情况


有四种场景会造成数据不一致:



针对场景1和2的解决方案是:保存请求对缓存的读取记录,延时消息比较,发现不一致后,做业务补偿 针对场景3和4的解决方案是:对于写请求,需要配合分布式锁使用。写请求进来时,针对同一个资源的修改操作,先加分布式锁,保证同一时间只有一个线程去更新数据库和缓存;没有拿到锁的线程把操作放入到队列中,延时处理。用这种方式保证多个线程操作同一资源的顺序性,以此保证一致性。



其中,分布式锁的实现可以使用以下策略:




(三)强一致性策略


上述策略只能保证数据的最终一致性。要想做到强一致,最常见的方案是2PC、3PC、Paxos、Raft这类一致性协议,但它们的性能往往比较差,而且这些方案也比较复杂,还要考虑各种容错问题。如果业务层要求必须读取数据的强一致性,可以采取以下策略:


  • 暂存并发读请求


在更新数据库时,先在Redis缓存客户端暂存并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性。


  • 串行化


读写请求入队列,工作线程从队列中取任务来依次执行


  • 修改服务Service连接池,id取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上。


  • 修改数据库DB连接池,id取模选取DB连接,能够保证同一个数据的读写在数据库层面是串行的。



  • 使用Redis分布式读写锁


将淘汰缓存与更新库表放入同一把写锁中,与其它读请求互斥,防止其间产生旧数据。读写互斥、写写互斥、读读共享,可满足读多写少的场景数据一致,也保证了并发性。并根据逻辑平均运行时间、响应超时时间来确定过期时间。


public void write() {    Lock writeLock = redis.getWriteLock(lockKey);    writeLock.lock();    try {        redis.delete(key);        db.update(record);    } finally {        writeLock.unlock();    }}
public void read() { if (caching) { return; } // no cache Lock readLock = redis.getReadLock(lockKey); readLock.lock(); try { record = db.get(); } finally { readLock.unlock(); } redis.set(key, record);}



(四)小结


针对读写缓存时:同步直写,更新数据库+更新缓存



针对只读缓存时:更新数据库+删除缓存



较为通用的一致性策略拟定:


在并发场景下,使用“更新数据库+更新缓存”需要用分布式锁保证缓存和数据一致性,且可能存在“缓存资源浪费”和“机器性能浪费”的情况;一般推荐使用“更新数据库+删除缓存”的方案。如果根据需要,热点数据较多,可以使用“更新数据库+更新缓存”策略。


在“更新数据库+删除缓存”的方案中,推荐使用推荐用“先更新数据库,再删除缓存”策略,因为先删除缓存可能会导致大量请求落到数据库,而且延迟双删的时间很难评估。


在“先更新数据库,再删除缓存”策略中,可以使用“消息队列+重试机制”的方案保证缓存的删除。并通过“订阅binlog”进行缓存比对,加上一层保障。


此外,需要通过初始化缓存预热、多数据源触发、延迟消息比对等策略进行辅助和补偿。【多种数据更新触发源:定时任务扫描,业务系统MQ、binlog变更MQ,相互之间作为互补来保证数据不会漏更新】



三、数据不一致性需注意其他问题


(一) k-v大小的合理设置


Redis key大小设计:由于网络的一次传输MTU最大为1500字节,所以为了保证高效的性能,建议单个k-v大小不超过1KB,一次网络传输就能完成,避免多次网络交互;k-v是越小性能越好


Redis热key:当业务遇到单个读热key,通过增加副本来提高读能力或是用hashtag把key存多份在多个分片中。


当业务遇到单个写热key,需业务拆分这个key的功能,属于设计不合理-当业务遇到热分片,即多个热key在同一个分片上导致单分片cpu高,可通过hashtag方式打散。



(二)避免其他问题导致缓存服务器崩溃,进而简直导致数据一致性策略失效缓存穿透、缓存击穿、缓存雪崩、机器故障等问题




(三)方案选定的思路


  • 确定缓存类型(读写/只读)


  • 确定一致性级别


  • 确定同步/异步方式


  • 选定缓存流程


  • 补充细节


参考资料:
1.
Redis与MySQL双写一致性如何保证

2.干货|携程最终一致和强一致性缓存实践

3.大厂都是怎么做MySQL to Redis同步的

4.缓存与数据库一致性策略

5.缓存与数据库一致性保证

6.如何解决缓存和数据库的数据不一致问题

7.Redis经典问题,缓存(穿透,雪崩,击穿,数据不一致,数据并发竞争,HotKey,BigKey),分布式锁(watch乐观锁,setnx,Redisson)

8.Redisson分布式锁场景和使用



 作者简介


徐鑫

腾讯后台开发工程师

腾讯CSIG智慧零售研发中心后台开发工程师,硕士研究生,毕业于中南大学。目前负责腾讯智慧零售业务的后端研发工作。


- EOF -


推荐阅读  点击标题可跳转

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

2、美团二面:Redis 与 MySQL 双写一致性如何保证?

3、分布式事务从入门到放弃:数据一致性引擎概览



看完本文有收获?请转发分享给更多人

推荐关注「数据分析与开发」,提升数据技能

点赞和在看就是最大的支持❤️

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/123188
 
365 次点击