Redis缓存双写一致性笔记(上)

news/2024/12/22 0:18:09/

Redis缓存双写一致性是指在将数据同时写入缓存(如Redis)和数据库(如MySQL)时,确保两者中的数据保持一致性。在分布式系统中,缓存通常用于提高数据读取的速度和减轻数据库的压力。然而,当数据更新时,如果没有适当的机制来同步缓存数据库,可能会导致用户读到的数据是过时的或不一致的。

1.缓存双写一致性的理解

如果redis有数据,需要和数据库中的值相同

如果redis无数据, 数据库中的值要是最新值,且准备回写redis

缓存按照操作来分,细分以下2种

  • 只读缓存

  • 读写缓存

    • 同步直写策略:写数据库之后也同步写redis缓存缓存数据库中的数据一致;

      对于读写缓存来说,要想保证缓存数据库中的数据一致,就要采用同步直写策略

    • 异步缓写策略:正常业务中,MySQL数据变了,但是可以在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统;异常情况出现了, 不得不将失败的动作重新修补,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重试重写

采用双检加锁策略

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存

如果数据库里没有,缓存空数据,避免大量请求发生缓存穿透

2.数据库缓存一致性的更新策略

目的:达到数据最终一致性

   给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。 我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准

上述方案和后续落地案例是调研后的主流+成熟的做法,但是考虑到各个公司业务系统的差距,不是100%绝对正确,不保证绝对适配全部情况,需要自己酌情选择打法,合适自己的最好。

可以停机的情况

挂牌报错,凌晨升级,温馨提示,服务降级

单线程,这样重量级的数据操作最好不要多线程

我们讨论4种更新策略

2.1 先更新数据库,在更新缓存(不可取)

异常问题1

  • 先更新mysql的某商品的库存,当前商品的库存是100,更新为99个。
  • 先更新mysql修改为99成功,然后更新redis
  • 此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100。
  • 上述发生,会让数据库里面和缓存redis里面数据不一致,读到redis脏数据

异常问题2

【先更新数据库,再更新缓存】﹐A、B两个线程发起调用
【正常逻辑】
1 A update mysql 100
2 A update redis 100
3 B update mysql 80
4 B update redis 80
=============================
【异常逻辑】
多线程环境下,A、B两个线程有快有慢,有前有后有并行
1 A update mysql 100
3 B update mysql 80
4 B update redis 80
2 A update redis 100
=============================
最终结果,mysql和lredis数据不一致
mysql:80,redis:100

2.2 先更新缓存,再更新数据库(不可取)

不推荐,业务上一般把MySQL作为底单数据库 ,保证最后解释

[先更新缓存,再更新数据库],A、B两个线程发起调用
[正常逻辑]
1 A update redis 100
2 A update mysql 100
3 B update redis 80
4 B update mysql 80
====================================
[异常逻辑]多线程环境下,A. B两个线程有快有慢有并行
A update redis 100
B update redis 80
B update mysq| 80
A update mysql 100
====================================
mysql:100,redis:80

2.3 先删除缓存,在更新数据库(不可取)

异常问题:

步骤分析,先删除缓存,再更新数据库

1 A线程先成功删除了redis里面的数据,然后去更新mysql,此时mysql正在更新中,还没有结束。(比如网络延时)
B突然出现要来读取缓存数据。2 此时redis里面的数据是空的,B线程来读取,先去读redis里数据(已经被A线程delete掉了),此处出来2个问题:
2.1 B从mysq|获得了旧值
B线程发现redis里没有(缓存缺失)马上去mysql里面读取,从数据库里面读取来的是旧值。
2.2 B会把获得的旧值写回redis
获得旧值数据后返回前台并回写进redis(刚被A线程删除的旧数据有极大可能早被写回了)。3 A线程更新完mysql,发现redis里面的缓存是脏数据,A线程直接懵逼了,o(T_ .τ)o两个并发操作,一个是更新操作,另一个是查询操作,A删除缓存后,B查询操作没有命中缓存,B先把老数据读出来后放到缓存中,然后A更新操作更新了数据库。
于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
4总结流程:
(1)请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql.....
A还么有彻底更新完mysql,还没commit
(2)请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)
(3)请求B继续,去数据库查询得到了mysq中的旧值(A还没有更新完)
(4)请求B将旧值写回redis缓存
(5)请求A将新值写入mysql数据库
上述情况就会导致不一致的情形出现。

先删除缓存,再更新数据库:如果数据库更新失败或超时或返回不及时,导致B线程请求访问缓存时发现redis里面没数据,缓存缺失,B再去读取mysql时,从数据库中读取到旧值,还写回redis, 导致A白干了

解决方案:采用延时双删策略 

加上sleep的这段时间,就是为了让线程B能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程A再进行删除。所以,线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。这样一来,其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做“延迟双删”。

延迟双删面试题

这个删除该休眠多久呢?线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。

这个时间怎么确定呢? 第一种方法: 在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。 这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。 第二种方法: 新启动一个后台监控程序,比如后面要讲解的WatchDog监控程序,会加时

这种同步淘汰策略,吞吐量降低怎么办?

2.4 先更新数据库,再删除缓存

异常问题

时间线程A线程B出现的问题
t1更新数据库中的值......
t2缓存立刻命中,此时B读取的是缓存旧值A还没来得及删除缓存的值,导致B缓存命中读到旧值
t3更新缓存的数据,over

先更新数据库,在删除缓存,假如缓存删除失败或者来不及删除,导致请求再次访问redis缓存命中,读取到的是缓存的旧值。

解决方案 :

  • 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。
  • 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
  • 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库缓存的数据一致了,否则还需要再次进行重试 4 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。

3. 双写一致总结

方案如何选择?利弊如何

在大多数业务场景下, 个人建议是,优先使用先更新数据库再删除缓存的方案(先更库→后删存)。理由如下:

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

2如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。

多补充一句:如果使用先更新数据库,再删除缓存的方案)

如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Redis缓存客户端暂停并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性,这是理论可以达到的效果,但实际,不推荐,因为真实生产环境中,分布式下很难做到实时一致性,一般都是最终一致性。

策略高并发多线程条件下问题现象解决方案
先删除redis缓存,再更新mysql缓存删除成功但数据库更新失败Java程序从数据库中读到旧值再次更新数据库,重试
缓存删除成功但数据库更新中... 有并发请求并发请求从数据库读到旧值并回写到redis,导致后续都是从redis读取到旧值再次删除缓存,重试
先更新mysql,再删除redis缓存数据库更新成功,但缓存删除失败Java程序从redis中读到旧值再次删除缓存,重试
数据库更新成功但缓存删除中...... 有并发读请求并发请求从缓存读到旧值等待redis删除完成,这段时间数据不一致,短暂存在。

4.最后

祝愿大家国庆快乐!

感谢大家,请大家多多支持!


http://www.ppmy.cn/news/1533553.html

相关文章

二、初步编写drf API

2.1基于django #settings.py urlpatterns [path(admin/, admin.site.urls),path(auth,views.auth) #创建一个路由 ]#views.py from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt# Create your views here.c…

GB28181信令交互流程及Android端设备对接探讨

GB28181规范必要性 好多开发者在做比如执法记录仪、智能安全帽、智能监控等设备端视频回传技术方案选型的时候,不清楚到底是用RTSP、RTMP还是GB28181,对GB28181相对比较陌生,我们就GB28181规范的必要性,做个探讨: 实现…

设计模式之访问者

一、访问者设计模式概念 访问者模式(Visitor) 是一种行为设计模式, 它能将算法与其所作用的对象隔离开来。 适用场景 如果你需要对一个复杂对象结构 (例如对象树) 中的所有元素执行某些操作, 可使用访问者模…

S32K312 RTD 4.0.0 版本 OCU 例程配置流程说明

一、前言 由于 RTD 4.0.0 版本并没有 S32K312 相关例程,本文基于已有的 S32K344 OCU 例程,新建 S32K312 工程,讲解 OCU 例程的相关配置流程。 二、基本概念 OCU(Output Compare Unit – 输出比较单元)本质上是一个计…

Spring Boot 入门指南

在软件开发中,Java 依然是一个流行的编程语言,而 Spring 框架则是 Java 生态系统中最受欢迎的框架之一。 Spring 提供了一个全面的编程和配置模型,使开发人员能够构建高效、可扩展的企业级应用程序。Spring Boot 是 Spring 的一个子项目&…

Linux中的软硬链接和动静态库

硬链接 ln myfile.txt hard_file.link 264962 -rw-rw-r-- 2 zhangsan zhangsan 0 Sep 30 03:16 hard_file.link 264962 -rw-rw-r-- 2 zhangsan zhangsan 0 Sep 30 03:16 myfile.txt 273922 lrwxrwxrwx 1 zhangsan zhangsan 10 Sep 30 03:17 soft_file.link -> …

[含文档+PPT+源码等]精品大数据项目-基于Django实现的高校图书馆智能推送系统的设计与实现

大数据项目——基于Django实现的高校图书馆智能推送系统的设计与实现背景,可以从以下几个方面进行详细阐述: 一、信息技术的发展背景 随着信息技术的飞速发展和互联网的广泛普及,大数据已经成为现代社会的重要资源。在大数据背景下&#xf…

django的模型层介绍与配置

1 Django的Model模型介绍 模型是我们项目中的的数据信息源,它包含着储存数据的必要字段和行为。 通常,每个模型对应数据库中的一张表,每个属性对应一个字段 每个模型都是django.db.models.Model的一个Python 子类。 Django 提供一套自动生成…