Redis和数据库的数据一致性问题研究

前言

在开发中为了减轻数据库的访问压力,使用Redis作为缓存基本是业内共识,但引入新的中间件,必然也会带来更高的复杂度,也就是数据一致性问题。如果Redis和数据库数据不一致,那么就会导致业务上拿到的数据是脏数据,导致系统异常。那么我们应该如何去保证数据的一致性呢?这就是本文要探讨的课题。

首先我们应该理解,Redis作为缓存,数据应该以数据库为准。为了保证一致性,我们更新数据库的同时就应该去更新缓存,这也没有争议,两者更新之间有时间间隔,那么这段时间内数据显然不一致。但是业务代码中完全同时更新是做不到的,因为Redis和数据库是相互独立的系统,一定会有一个先后顺序。因此,我们需要考虑的就是谁先谁后的问题了。

不过在讲写流程之前,我们先把简单的解决,那就是读流程, 读流程是没有争议的。

步骤

1.收到读请求

2.1如果缓存有数据,从缓存中读数据,直接返回数据

2.2如果缓存没有数据,读取数据库中的数据到内存

3.将数据库的数据放入缓存

4.返回数据

按这个流程,无论缓存有没有数据,都不影响正常流程。

接下来来看写流程

写流程有4种方案
1)先更新数据库,再更新缓存
2)先更新缓存,再更新数据库
3)先更新数据库,再删除缓存
4)先删除缓存,再更新数据库

这四种方案大致讨论的是,先处理数据库还是先处理缓存,是直接更新缓存好还是删除缓存好。

如何判断好不好,就看这个方案在高并发下保证数据一致性的能力如何

我们来看看写写并发 的情况
1)先更新数据库,再更新缓存

1线程A更新数据库为1

2线程B更新数据库为2

3线程B更新缓存为2

4线程A更新缓存为1

最终数据库数据为2,缓存数据为1,数据不一致
2)先更新缓存,再更新数据库

1线程A更新缓存为1

2线程B更新缓存为2

3线程B更新数据库为2

4线程A更新数据库为1

最终数据库数据为1,缓存为2,数据不一致

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

1线程A更新数据库为1

2线程B更新数据库为2

3线程B删除缓存

4线程A删除缓存

最终数据库值为2,没有缓存,下次查询时,会将最新的数据放到缓存中,数据一致

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

1线程A删除缓存

2线程B删除缓存

3线程B更新数据库为2

4线程A更新数据库为1

最终数据库值为1,没有缓存,下次查询时,会将最新的数据放到缓存中,数据一致

可以看到写写并发时,只有方案三和方案四能保证数据一致

那么读写并发时呢
1)先更新数据库,再更新缓存

1线程A更新数据库为1

2线程B读取数据,缓存中有数据,从缓存中获取旧数据

3线程A更新缓存为1

最终数据库数据为1,缓存数据为1,数据一致
2)先更新缓存,再更新数据库

1线程A更新缓存为1

2线程B读取数据,缓存中有数据,从缓存中获取新数据

4线程A更新数据库为1

最终数据库数据为1,缓存为2,数据一致
3)先更新数据库,再删除缓存

0.数据库初始值为1

1.线程A更新数据库为2

2.线程B获取数据,缓存有数据,从缓存中获取旧数据

3.线程A删除缓存

最终数据库为2,没有缓存,下次查询时,缓存获取最新值。数据一致
4)先删除缓存,再更新数据库

0.数据库初始值为1

1.线程A删除缓存

2.线程B获取数据,缓存无数据,从数据库获取值为1

3.线程B将数据1加载到缓存中

4.线程A更新数据库为2

最终数据库为2,缓存中为1。数据不一致

读写并发时,方案一,方案二,方案三可以保证数据一致性

可以看到无论是写写并发还是读写并发,方案三都能保证最终一致性。

这就是是现在最常用,最经典,开发成本最低的方案------Cache-Aside Pattern(旁路缓存模式)

Cache-Aside Pattern(旁路缓存策略)

最常用、最经典的缓存模式。
读流程

1.读请求

2.如果缓存有数据,从缓存中读数据,直接返回数据

3.如果缓存没有数据,读取数据库中的数据

4.将数据库的数据放入缓存,返回数据

写流程

1.写请求

2.更新数据库中的数据

3.删除缓存

那么,这个方案真的没问题吗?非也。

其实上述四种方案在读写并发时,都有一个共同的弊端。我们来看下面这个场景

如果缓存中无数据,发生读写并发时

0.数据库初始值为1

1.线程A获取数据,缓存无数据,从数据库获取值为1

2.线程B更新数据库和缓存为2(无论先后,无论删除或更新),线程B更新操作执行完毕

3.线程A将数据库取到的值1放入缓存中

最终数据库值为2,缓存值为1。导致脏数据,没有保证最终一致性。

可以看到,这种情况下,无论使用哪种方案,都无法完全保证最终一致性。

不可否认,这种情况概率比较低,必须在线程A从数据库拿到旧数据,和将旧数据保存到缓存的中间,完成另一个线程的写操作,才会导致数据不一致。所以对数据一致性要求没那么高的系统使用旁路缓存模式就够了。

但如果一致性要求真的更高的话,该怎么办呢?

延时双删策略

缓存双删策略是一种保证最终一致性的方案,读流程不变,写流程增加了一次删除操作

写流程

1.写请求

2.删除缓存

3.更新数据库

4.休眠一段时间(一次读取操作消耗的时间,比如500毫秒)

5.删除缓存

旁路缓存策略中,无法保证数据一致性的场景是线程A从数据库获取旧数据,在将旧数据放入缓存之前,一个新的写操作已经完成,此时再将旧数据放入缓存,就会导致数据不一致。

那么我们在更新数据库之后,将线程休眠一段时间,保证其他线程可以完成读取操作就可以了。那么这个休眠时间是多久呢?也很简单,我们只要保证其他线程可以完成读取操作即可,因此休眠时间就应该是一次读操作的耗时。

我们来看看加入休眠操作后的情况
步骤

1.线程A读取旧数据

2.线程B更新数据库

3.线程B休眠500ms

4.线程A将旧数据保存到缓存中

5.线程B删除缓存

可以看到,因为线程B休眠了500ms,所以删除操作几乎一定会在最后执行。也因为是删除操作,所以下次读取一定是最新值,因此保证了最终一致性。

但由于500ms里一直没有删除缓存,那么如果缓存里有旧数据,就会导致这500ms里一直都是脏数据。因此,我们可以再加入一次删除缓存的操作,尽量保证这500ms里缓存里的数据也是最新的。

总结

大部分普通场景下,旁路缓存策略已经够用了,对数据一致性要求更高的场景下,可以使用延时双删策略。

相关推荐
计算机安禾1 天前
【数据库系统原理】第19篇:计算机存储层次结构与数据库文件的物理组织
数据库·oracle
JAVA面经实录9171 天前
操作系统面试题
java·服务器·数据库·计算机网络·面试
摇滚侠1 天前
mariadb-libs 被 mysql-community-libs-5.7.28-1.el7.x86_64 取代
数据库·mysql·mariadb
不能只会打代码1 天前
边缘视频分析平台的架构设计与性能优化——从750ms到190ms的调优之路
java·spring boot·redis·性能优化·边缘计算·物联网竞赛
DIY源码阁1 天前
JavaSwing饮品管理系统 - MySQL版
java·数据库·mysql·eclipse
专注搞钱1 天前
GPT-4o写设备Recipe:从3小时到10分钟
数据库·人工智能·gpt·半导体
东风破1371 天前
达梦数据库实战:备份恢复与数据迁移全攻略(实例初始化、服务注册、路径迁移)
数据库·chrome
SelectDB技术团队1 天前
2026 SelectDB AI 产品发布会:Agent Native 数据基础设施能力全景发布
数据库·人工智能·agent·apache doris·selectdb
爱吃羊的老虎1 天前
【数据库】模块一:数据库基础与关系代数
数据库
dishugj1 天前
iSCSI + Multipath + ASM:Oracle RAC 共享存储技术链详解
数据库·oracle