前言

使用redis作为缓存,必然存在redis缓存和DB数据一致性的问题:某一时刻,redis缓存数据和DB数据不一致

一 缓存更新策略

按照缓存更新的方式大致分为: 内存淘汰、过期删除、主动更新

一) 内存淘汰

利用Redis的内存淘汰策略,当内存不足时自动进行淘汰部分数据,下次查询时更新缓存,一致性差,无维护成本

内存淘汰策略详情请参考:redis内存淘汰策略和过期删除策略

二) 过期删除

缓存添加过期时间,到期后根据过期删除策略自动进行删除缓存,下次查询时进行更新缓存,一致性一般,维护成本低

过期删除策略详情请参考:redis内存淘汰策略和过期删除策略

三) 主动更新

应用程序中修改DB,修改缓存,一致性好,维护成本高

主动更新大致分为: Cache Aside Pattern、Read/Write Through Pattern、Write Behind Caching Pattern

1 Cache Aside Pattern

即旁路缓存模式,旁路路由策略,最经典常用的缓存策略

应用程序负责缓存和DB的读写

读写操作步骤:
读操作时,先读缓存,缓存存在直接返回;缓存不存在则读DB,然后把读的DB数据存入缓存,返回

写操作时,先更新DB,再删除缓存

读操作流程图:

写操作流程图:

2 Read/Write Through Pattern

该模式下应用程序直接和缓存管理组件交互,缓存管理组件和DB交互,无需关心缓存一致性问题

应用程序只与缓存管理组件交互,负责缓存的读写,缓存管理组件负责DB的读写

1) Read Through

读操作时,缓存管理组件先读缓存,缓存存在直接返回;缓存不存在则读DB,然后把读的DB数据存入缓存,返回

流程图:

2) Write Through

写操作时,缓存管理组件同步更数DB和缓存

流程图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-057lcnOf-1678262363678)(/Write Through流程图.png)]

3 Write Behind Caching Pattern

和 Write Through相似,不同点在于Write Through更新DB和更新缓存是同步的,而Write Behind Caching Pattern更新DB和更新缓存是异步的
应用程序只与缓存管理组件交互操作,负责缓存的读写,通过定时或阈值的异步方式将数据同步到DB,保证最终一致

读流程图:

写流程图:

优点:
减少了更新DB的频率,读写响应非常快,吞吐量也会有明显的提升

缺点:
不能时时同步,数据同步DB过程服务不可用,导致数据丢失

4 三种主动更新策略的对比

策略说明优点缺点
Cache Aside Pattern应用程序负责缓存和DB的读写使用简单,直接操作缓存和DB需要编写对缓存和DB读写的代码
Read/Write Through Pattern应用程序只与缓存管理组件交互,负责缓存的读写,缓存管理组件负责DB的读写只需负责缓存的读写复杂,需要提供对DB读写的handler
Write Behind Caching Pattern应用程序只与缓存管理组件交互,负责缓存的读写,缓存管理组件负责DB的读写,性能最好,在高并发场景下可以降低数据库的压力性能最好;只需负责缓存的读写不能时时同步,数据同步DB过程服务不可用,导致数据丢失

四) 三种缓存更新策略的对比

策略说明一致性维护成本
内存淘汰使用Redis的内存淘汰策略,当内存不足时自动进行淘汰部分数据,下次查询时更新缓存
过期删除缓存添加过期时间,到期后根据过期删除策略自动进行删除缓存,下次查询时进行更新缓存
主动更新在修改数据库的时也修改缓存,使用硬编码方式或者硬编码+中间件方式在修改数据库时同步或异步的修改缓存

二 更新缓存的两种方式1 删除缓存

更新DB时删除缓存,查询时再从DB中读取数据并更新到缓存

2 更新缓存

更新DB时更新缓存,频繁更新缓存开销大,且并发时可能导致请求读取的缓存数据是旧数据

三 缓存更新策略的实现方式一) 先更新缓存,再更新DB1 并发写场景

所有线程都是先更新缓存再更新DB,在某个写线程更新缓存和更新DB之间,其他写线程也更新了缓存和DB,导致缓存和DB数据不一致

流程图:

具体步骤:
1) 线程1更新缓存
2) 线程2更新缓存
3) 线程2更新DB
4) 线程1更新DB

缓存和DB数据不一致的原因:
理论上先更新缓存的线程也会先更新DB,但是并发场景下线程的执行顺序无法保证:
a) 若更新DB的顺序是: 线程1再线程2,则不会出现数据不一致问题
b) 若更新DB的顺序是: 线程2再线程1,此时缓存是线程2的数据,DB是线程1的数据,导致缓存和DB数据不一致

先删除缓存再更新DB—-并发读写场景流程图2 并发读写场景

在写线程更新缓存和更新DB之间,读线程也可以获取到最新的缓存,不会导致缓存和DB数据不一致

流程图:

具体步骤:
1) 线程1更新缓存
2) 线程2查询,命中缓存返回
3) 线程1更新DB

缓存和DB数据不一致的原因:
可以保证缓存和DB数据一致,虽然线程1更新DB的操作还没有完成,但是更新缓存的操作已经完成了,读请求可以获取到最新的缓存

二) 先更新DB,再更新缓存1 并发写场景

所有线程都是先更新DB再更新缓存,在某个写线程更新DB和更新缓存之间,其他写线程也更新了DB和缓存,导致缓存和DB数据不一致

流程图:

具体步骤:
1) 线程1更新DB
2) 线程2更新DB
3) 线程2更新缓存
4) 线程1更新缓存

缓存和DB数据不一致的原因:
理论上先更新DB的线程也会先更新缓存,但是并发场景下线程的执行顺序无法保证:
a) 若更新缓存的顺序是: 先线程1再线程2,则不会出现数据不一致问题
b) 若更新缓存的顺序是: 先线程2再线程1,此时缓存是线程1的数据,DB是线程2的数据,导致缓存和DB数据不一致

2 并发读写场景

在写线程更新DB和更新缓存之间,读线程可以获取到旧数据,但最终会一致

流程图:

具体步骤:
1) 线程1更新DB
2) 线程2查询,命中缓存返回
3) 线程1更新缓存

缓存和DB数据不一致的原因:
线程2获取的缓存是旧数据,但最终都会一致

三) 先删除缓存,再更新DB1 并发写场景

所有线程都是先删除缓存再更新DB,无论哪个线程先删除缓存再更新DB,缓存都会被删除,不会导致缓存和DB数据不一致

流程图:

具体步骤:
1) 线程1删除缓存
2) 线程2删除缓存
3) 线程2更新DB
4) 线程1更新DB

缓存不一致原因:
无论哪个线程先删除缓存再更新DB,缓存都会被删除,不会导致缓存和DB数据不一致

2 并发读写场景

在写线程删除缓存和更新DB之间,读线程根据查询的DB结果更新了缓存,导致缓存和DB数据不一致

流程图:

具体步骤:
1) 线程1删除缓存
2) 线程2查询,未命中
3) 线程2查询DB
4) 线程2根据查询的DB结果更新缓存
5) 线程1更新DB

缓存和DB数据不一致的原因:
线程1删除缓存和更新DB之间,线程2根据查询的DB结果更新了缓存,导致缓存和DB数据不一致

四) 先更新DB,再删除缓存1 并发写场景

所有线程都是先更新DB再删除缓存,无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致

流程图:

具体步骤:
1) 线程1更新DB
2) 线程2更新DB
3) 线程2删除缓存
4) 线程1删除缓存

缓存不一致原因:
无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致

2 并发读写场景

在写线程更新DB再删除缓存之间,读线程可以获取到旧数据,但最终会一致

流程图:

具体步骤:
1) 线程1更新DB
2) 线程2查询命中缓存返回
3) 线程1删除缓存

缓存不一致原因:
线程2获取的缓存是旧数据,但最终都会一致

五) 延迟双删

先删除缓存再更新DB在并发写场景不会导致数据不一致,但是在并发读写场景会导致数据不一致
延迟双删是基于先删除缓存再更新DB的基础上的改进:在更新DB后延迟一定时间,再次删除缓存

延时是为了保证第二次删除缓存前能完成更新DB操作,延时时间根据系统的查询性能而定
第二次删除缓存是为了保证后续请求查询DB(此时数据库中的数据已是更新后的数据),重新写入缓存,保证数据一致性

1 并发写场景

无论哪个线程都会删除缓存,不会导致缓存和DB数据不一致

流程图:

具体步骤:
1) 线程1删除缓存
2) 线程2删除缓存
3) 线程2更新DB
4) 线程1更新DB
5) 线程1延时删除缓存
6) 线程2延时删除缓存

缓存不一致原因:
无论哪个线程都会删除缓存,不会导致缓存和DB数据不一致

2 并发读写场景

流程图:

具体步骤:
1) 线程1删除缓存
2) 线程2查询,未命中
3) 线程2查询DB
4) 线程2根据查询的DB结果更新缓存
5) 线程1更新DB
6) 线程1延时删除缓存

缓存不一致原因:
线程1第一次删除缓存之后,线程2根据查询的DB结果更新缓存,此时查询得到的结果是旧数据,线程1延迟第二次删除缓存之后,后续查询DB(此时数据库中的数据已是更新后的数据),重新写入缓存,不会导致缓存和DB数据不一致

3 延时双删的缺点

1) 需要延时,低延时场景不合适,如秒杀等需要低延时,需要强一致,高频繁修改数据场景
2) 不能保证强一致性,在更新DB之前,查询线程查询得到的结果是旧数据,可但可以减轻缓存和DB数据不一致的问题
3) 延时的时间是一个不可评估的值,延时越久,能规避一致性的概率越大

六) 异步删除缓存

先更新DB再删除缓存在并发写场景不会导致数据不一致,但是在并发读写场景会短暂的导致数据不一致,但是由于删除缓存失败不会重试,并发写场景、并发读写场景都可能长时间导致数据不一致

异步更新缓存是基于先更新DB再删除缓存的基础上的改进:更新DB之后,基于消费队列异步删除缓存

根据消费队列不同大致分为:消息队列、binlog+消息队列

1 基于消息队列的异步删除缓存1) 并发写场景

无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致

流程图:

具体步骤:
1) 线程1更新DB
2) 线程2更新DB
3) 线程2把删除缓存放入消息队列
4) 线程1把删除缓存放入消息队列
5) 异步:消息队列消费删除缓存

缓存不一致原因:
无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致

2) 并发读写场景

异步删除缓存期间,读线程获取的缓存是旧数据,短暂出现数据不一致,异步删除缓存后最终会一致

流程图:

具体步骤:
1) 线程1更新DB
2) 线程2查询缓存,命中返回
3) 线程1把删除缓存放入消息队列
4) 异步:消息队列消费删除缓存

缓存不一致原因:
异步删除缓存期间,读线程获取的缓存是旧数据,短暂出现数据不一致,异步删除缓存后最终会一致

2 基于binlog+消息队列删除缓存1) 并发写场景

流程图:

具体步骤:
1) 线程1更新DB
2) 线程2更新DB
3) 异步:binlog日志收集中间件定时收集DB的binglog日志
4) 异步:binlog日志收集中间件发送日志消息到消息队列
5) 异步:消息队列消费删除缓存

缓存不一致原因:
无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致

2) 并发读写场景

流程图:

具体步骤:
1) 线程1更新DB
2) 线程2查询缓存,命中返回
3) 异步:binlog日志收集中间件定时收集DB的binglog日志
4) 异步:binlog日志收集中间件发送日志消息到消息队列
5) 异步:消息队列消费删除缓存

缓存不一致原因:
异步删除缓存期间,读线程获取的缓存是旧数据,短暂出现数据不一致,异步删除缓存后最终会一致

3 异步删除缓存的优缺点

优点
1) 删除缓存的操作与主流程代码解耦
2) 中间件自带重试机制,增加了操作缓存的成功率

缺点
引入中间件,提升了系统的复杂度,在高并发场景可能会产生性能问题

七) 几种实现方式的对比

策略并发场景并发问题数据不一致概率说明
先更新缓存,再更新DB并发写所有线程都是先更新缓存再更新DB,在某个写线程更新缓存和更新DB之间,其他写线程也更新了缓存和DB,导致缓存和DB数据不一致
并发读写在写线程更新缓存和更新DB之间,读线程也可以获取到最新的缓存,不会导致缓存和DB数据不一致不会出现
先更新DB,再更新缓存并发写所有线程都是先更新DB再更新缓存,在某个写线程更新DB和更新缓存之间 其他写线程也更新了DB和缓存,此时缓存和DB数据不一致
并发读写在写线程更新DB和更新缓存之间,读线程获取的缓存是旧数据,短暂出现数据不一致,但最终会一致短暂不一致,最终会一致
先删除缓存,再更新DB并发写无论哪个线程先删除缓存再更新DB,缓存都会被删除,不会导致缓存和DB数据不一致不会出现
并发读写在写线程删除缓存和更新DB之间,读线程根据查询的DB结果更新了缓存,导致缓存和DB数据不一致
先更新DB,再删除缓存并发写无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致不会出现
并发读写在写线程更新DB和删除缓存之间,读线程获取的缓存是旧数据,短暂出现数据不一致,但最终会一致短暂不一致,最终会一致
延迟双删并发写无论哪个线程都会删除缓存,不会导致缓存和DB数据不一致不会出现延迟双删基于先删除缓存再更新DB的基础上的改进:在更新DB后延迟一定时间,再次删除缓存
并发读写在写线程删除缓存和更新DB之间,读线程根据查询的DB结果更新了缓存,短暂出现数据不一致,但延时再次删除缓存后数据会一致短暂不一致,最终会一致
异步删除缓存并发写无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致不会出现异步更新缓存是基于先更新DB再删除缓存的基础上的改进:更新DB之后,基于消费队列异步删除缓存
并发读写异步删除缓存期间,读线程获取的缓存是旧数据,短暂出现数据不一致,异步删除缓存后最终会一致短暂不一致,最终会一致