前言:

首先,缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在读取缓存方面,大家没啥疑问,都是按照下图的流程来进行业务操作。

        但是在更新缓存方面,对于更新完数据库,是更新缓存呢,还是删除缓存呢?又或者是先删除缓存,再更新数据库呢?其实大家存在很大的争议。本文主要针对不同的更新进行总结,

文章结构由以下三个部分组成:

1、讲解缓存更新策略

2、对每种策略进行缺点分析

3、针对缺点给出改进方案


先做一个说明,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。在这里,我们讨论三种更新策略:

  1. 先更新数据库,再更新缓存

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

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

应该没人问我,为什么没有先更新缓存,再更新数据库这种策略。

一、先更新数据库,再更新缓存

这套方案,大家是普遍反对的。主要有以下两个原因:

1、原因一:

从线程安全角度看,同时有请求A和请求B进行更新操作,那么会出现:

  • (1)线程A更新了数据库
  • (2)线程B更新了数据库
  • (3)线程B更新了缓存
  • (4)线程A更新了缓存

这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑

2、原因二:

从业务场景角度看,存在以下两个问题:

(1)如果是一个数据库写多读少的业务场景求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。

(2)如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。

接下来讨论的就是争议最大的,先删缓存,再更新数据库。还是先更新数据库,再删缓存的问题

二、先删缓存,再更新数据库:

1、存在问题:

该方案会导致不一致的原因是:同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:

  • (1)请求A进行写操作前,先删除缓存
  • (2)请求B查询发现缓存不存在
  • (3)请求B去数据库查询得到旧值
  • (4)请求B将旧值写入缓存
  • (5)请求A将新值写入数据库

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

2、解决方案:延时双删策略:

public void write(String key,Object data){

redis.delKey(key);

db.updateData(data);

Thread.sleep(1000);

redis.delKey(key);

}

转化为中文描述就是:

  • (1)先淘汰缓存
  • (2)再写数据库
  • (3)休眠1秒,再次淘汰缓存

这么做的目的是将休眠时间内产生的缓存脏数据再次删除(这个休眠时间需要具体根据项目的业务逻辑耗时指定)

3、如果是 MySQL 的读写分离架构怎么办?

在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。

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

上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms

4、采用延时双删除策略,吞吐量降低怎么办?

可以另起一个线程异步执行第二次删除操作,这样写的请求就不用沉睡一段时间后再返回了,从而加大吞吐量。但是如果第二次删除的时候,删除失败怎么办呢?如果第二次删除失败,就会出现如下情形。还是有两个请求,一个请求A进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库:

  • (1)请求A进行写操作,删除缓存
  • (2)请求B查询发现缓存不存在
  • (3)请求B去数据库查询得到旧值
  • (4)请求B将旧值写入缓存
  • (5)请求A将新值写入数据库
  • (6)请求A试图去删除请求B写入对缓存值,结果失败了

也就是说,如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。如何解决呢?具体解决方案,且看对第(3)种更新策略的解析。

三、先更新数据库,再删缓存:

首先,国外开发者提出了一个缓存更新策略名为《Cache-Aside pattern》,其中指出:

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

另外,知名社交网站facebook也在论文《Scaling Memcache at Facebook》中提出,他们用的也是先更新数据库,再删缓存的策略。

        但是这种策略并不是不存在并发问题,如果发生下述情况,还是会产生脏数据的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生:

  • (1)缓存刚好失效
  • (2)请求A查询数据库,得一个旧值
  • (3)请求B将新值写入数据库
  • (4)请求B删除缓存
  • (5)请求A将查到的旧值写入缓存

但是发生上述脏数据的情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。但是数据库的读操作的速度一般是远快于写操作的,因此步骤(3)耗时比步骤(2)更短,所以这一情形很难出现。

如果一定要解决怎么办?首先,给缓存设有效时间是一种方案。其次,采用策略(2)里给出的异步延时删除策略,保证读请求完成以后,再进行删除操作。

       最后,缓存更新策略二和缓存更新策略三都存在一个问题,如果删缓存失败了,还是会出现数据不一致的情况。针对这种情况,只要提供一个保障的重试机制即可,这里给出两套方案:

方案一:

流程如下所示:

  • (1)更新数据库数据;
  • (2)缓存因为种种问题删除失败
  • (3)将需要删除的key发送至消息队列
  • (4)自己消费消息,获得需要删除的key
  • (5)继续重试删除操作,直到成功

然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。

方案二:

流程如下图所示:

  • (1)更新数据库数据
  • (2)数据库会将操作信息写入binlog日志当中
  • (3)订阅程序提取出所需要的数据以及key
  • (4)另起一段非业务代码,获得该信息
  • (5)尝试删除缓存操作,发现删除失败
  • (6)将这些信息发送至消息队列
  • (7)重新从消息队列中获得该数据,重试操作

备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。另外,重试机制,博主是采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。

Redis缓存与数据库双写一致性相关推荐

  1. 掌握分布式环境缓存更新策略,提高缓存与数据库双写一致性!

    概述 随着时代的发展,服务系统架构也已经由最初的单体架构转变为分布式.微服务架构模式. 从数据体量上来看,各系统存储的数据量越来越大,数据的查询性能越来越低. 此时,就需要我们不断的进行优化,最常用的 ...

  2. 高并发高可用复杂系统中的缓存架构(十六) 实现缓存与数据库双写一致性保障方案

    再来回顾下之前的思路: 数据更新:根据唯一标识路由到一个队里中,「删除缓存 + 更新数据」 数据读取:如果不在缓存中,根据唯一标识路由到一个队里中,「读取数据 + 写入缓存」 投入队里之后,就等待结果 ...

  3. 教你从0到1搭建秒杀系统-缓存与数据库双写一致

    本文是秒杀系统的第四篇,我们来讨论秒杀系统中缓存热点数据的问题,进一步延伸到数据库和缓存的双写一致性问题. 在秒杀实际的业务中,一定有很多需要做缓存的场景,比如售卖的商品,包括名称,详情等.访问量很大 ...

  4. Redis缓存穿透-热点缓存并发重建-缓存与数据库双写不一致-缓存雪崩

    解决缓存问题 1.解决Redis把内存爆满的三种方法 1.1 定期删除 1.2 惰性删除 1.3 内存淘汰策略 2. 缓存穿透--缓存击穿--缓存雪崩 3. 如何解决线上缓存穿透问题 3.1 缓存击穿 ...

  5. session.merge 缓存不更新_如何保证缓存与数据库双写时的数据一致性?

    在做系统优化时,想到了将数据进行分级存储的思路.因为在系统中会存在一些数据,有些数据的实时性要求不高,比如一些配置信息.基本上配置了很久才会变一次.而有一些数据实时性要求非常高,比如订单和流水的数据. ...

  6. 高并发下缓存与数据库双写不一致解决方案

    高并发下缓存与数据库双写不一致解决方案 参考文章: (1)高并发下缓存与数据库双写不一致解决方案 (2)https://www.cnblogs.com/wlwl/p/11601632.html (3) ...

  7. java的for循环取出数据只是拿到最后一个_如何保证缓存与数据库双写的一致性...

    Cache Aside Pattern ​ 最经典的缓存+ 数据库读写的模式,就是这个Cache Aside Pattern 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时 ...

  8. php双写绕过,高并发下缓存与数据库双写不一致解决方案

    解决方案 在高并发场景下,数据库和缓存双写不一致情况,我们可以当写入数据库后删除缓存,当查的时候先查缓存,如果缓存为空再查数据库,最后写入缓存,但是这样还是存在一个问题. 如图所示,当出现这种情况时该 ...

  9. Reids面试题集合 数据结构+穿透雪崩+持久化+内存淘汰策略+数据库双写+哨兵

    问题1 你对redis有什么了解? 简单的来说redis 就是一个数据库不同的是redsi的数据是存在内存中的,所以读写速度非常快因此,redis被广泛运用到缓存,另外redis也用来做分布式锁,re ...

最新文章

  1. 小程序input实现数据双向绑定
  2. 我爱计算机视觉干货集锦分类汇总(2019年5月7日)
  3. php+go+to,让phpstrom支持codeigniter框架实现 (GO TO )转到定义的功能
  4. Java EE重命名为Jakarta EE:Java EE Guardians与Oracle的分歧
  5. 微软提供Windows USB/DVD Download Tool:刻录WINDOWS安装系统到U盘
  6. 《FLUENT 14流场分析自学手册》——2.3 FLUENT14.5软件包的安装以及运行
  7. android格式化sd卡软件,SD/TF卡格式化程序
  8. R语言使用ltm包计算cronbach‘s alpha(克朗巴哈系数法)实战:cronbach alpha(克朗巴哈系数法)是一种测量问卷或调查内部一致性的方法、cronbach‘s alpha解读
  9. 学习笔记1——常用的注意力机制(即插即用)
  10. 给IDEA换个酷炫的主题,真的太好看了!
  11. soul显示服务器异常,soul账号状态异常是什么意思?soul账号异常操作多久解封
  12. php $_SERVER 学习详解
  13. 【Arduino】入门篇——人体红外自动报警
  14. iPhone、iPod和iPad离线固件升级的方法
  15. 修改计算机用户名bat,修改计算机名.bat
  16. 什么是安卓马甲APP?寻求高手安卓马甲包封装APP上架
  17. oracle查询值的字符串长度、字节长度、大小写字母转换
  18. 图片处理系列一Android照片墙应用实现(绝对不崩溃)
  19. Python配置新环境时,复制已经安装好虚拟环境的三种方法
  20. 数学之美-【算法】 - 用来流方式计算UV的基数算法

热门文章

  1. 五十二、微信小程序云开发中的云存储
  2. 多学科可行法matlab,微小卫星多学科建模与仿真方法研究
  3. 转载【IDEA】向IntelliJ IDEA创建的项目导入Jar包的两种方式
  4. 小米知识图谱团队斩获CCKS 2020实体链指比赛冠军
  5. 知识图谱理论与实践(以问答系统为例)
  6. 小马智行Pony.ai 2020校招宣讲行程来了!
  7. 神经机器阅读理解最新综述:方法和趋势
  8. A flight (to Boston) to Denver - 基于转移的顺滑技术研究 | 论文访谈间 #22
  9. python翻译成计算机是啥_基于Python的业英语翻译器实现
  10. 队列的基本操作_如果让你手写个栈和队列,你还会写吗?||CSDN博客精选