使用redis作为缓存,必然存在redis缓存和DB数据一致性的问题:某一时刻,redis缓存数据和DB数据不一致
按照缓存更新的方式大致分为: 内存淘汰、过期删除、主动更新
利用Redis的内存淘汰策略,当内存不足时自动进行淘汰部分数据,下次查询时更新缓存,一致性差,无维护成本
内存淘汰策略详情请参考:redis内存淘汰策略和过期删除策略
缓存添加过期时间,到期后根据过期删除策略自动进行删除缓存,下次查询时更新缓存,一致性一般,维护成本低
过期删除策略详情请参考:redis内存淘汰策略和过期删除策略
应用程序中修改DB,修改缓存,一致性好,维护成本高
主动更新大致分为: Cache Aside Pattern、Read/Write Through Pattern、Write Behind Caching Pattern
即旁路缓存模式,旁路路由策略,最经典常用的缓存策略
应用程序负责缓存和DB的读写
读写操作步骤:
读操作时,先读缓存,缓存存在直接返回;缓存不存在则读DB,然后把读的DB数据存入缓存,返回
写操作时,先更新DB,再删除缓存
即读写穿透,该模式下应用程序只与缓存管理组件交互,缓存管理组件负责缓存和DB的读写
读操作时,缓存管理组件先读缓存,缓存存在直接返回;缓存不存在则读DB,然后把读的DB数据存入缓存,返回
写操作时,缓存管理组件同步更数DB和缓存
即异步缓存写入,该模式下应用程序只与缓存管理组件交互操作,缓存管理组件负责缓存和DB的读写,通过定时或阈值的异步方式将数据同步到DB,保证最终一致
该模式和Read/Write模式相似,不同点在于Read/Write模式更新DB和更新缓存是同步的,而Write Behind Caching Pattern模式更新DB和更新缓存是异步的
优点:
减少了更新DB的频率,读写响应非常快,吞吐量也会有明显的提升
缺点:
不能时时同步,数据同步DB过程服务不可用,导致数据丢失
策略 | 说明 | 优点 | 缺点 |
---|---|---|---|
Cache Aside Pattern | 应用程序负责缓存和DB的读写 | 使用简单,直接操作缓存和DB | 需要编写对缓存和DB读写的代码 |
Read/Write Through Pattern | 应用程序只与缓存管理组件交互,缓存管理组件负责缓存和DB的读写 | 使代码更简洁 | 缓存管理组件需要提供对DB和缓存读写的方法 |
Write Behind Caching Pattern | 应用程序只与缓存管理组件交互,缓存管理组件负责缓存和DB的读写 | 性能最好,在高并发场景下可以降低数据库的压力 | 缓存管理组件 需要提供对DB和缓存读写的方法;不能时时同步,数据同步DB过程DB不可用,导致数据丢失;一致性不强,对一致性要求高的系统不适用 |
策略 | 说明 | 一致性 | 维护成本 |
---|---|---|---|
内存淘汰 | 使用Redis的内存淘汰策略,当内存不足时自动进行淘汰部分数据,下次查询时更新缓存 | 差 | 无 |
过期删除 | 缓存添加过期时间,到期后根据过期删除策略自动进行删除缓存,下次查询时进行更新缓存 | 低 | 低 |
主动更新 | 修改数据库时也修改缓存,使用硬编码方式或者硬编码+中间件方式在修改数据库时同步或异步的修改缓存 | 好 | 高 |
更新DB时删除缓存,查询时再从DB中读取数据并更新到缓存
更新DB时更新缓存,频繁更新缓存开销大,且并发时可能导致请求读取的缓存数据是旧数据
所有线程都是先更新缓存再更新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之间,读线程也可以获取到最新的缓存,不会导致缓存和DB数据不一致
具体步骤:
1) 线程1更新缓存
2) 线程2查询,命中缓存返回
3) 线程1更新DB
缓存和DB数据不一致的原因:
可以保证缓存和DB数据一致,虽然线程1更新DB的操作还没有完成,但是更新缓存的操作已经完成了,读请求可以获取到最新的缓存
所有线程都是先更新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数据不一致
在写线程更新DB和更新缓存之间,读线程可以获取到旧数据,但最终会一致
具体步骤:
1) 线程1更新DB
2) 线程2查询,命中缓存返回
3) 线程1更新缓存
缓存和DB数据不一致的原因:
线程2获取的缓存是旧数据,但最终都会一致
所有线程都是先删除缓存再更新DB,无论哪个线程先删除缓存再更新DB,缓存都会被删除,不会导致缓存和DB数据不一致
具体步骤:
1) 线程1删除缓存
2) 线程2删除缓存
3) 线程2更新DB
4) 线程1更新DB
缓存不一致原因:
无论哪个线程先删除缓存再更新DB,缓存都会被删除,不会导致缓存和DB数据不一致
在写线程删除缓存和更新DB之间,读线程根据查询的DB结果更新了缓存,导致缓存和DB数据不一致
具体步骤:
1) 线程1删除缓存
2) 线程2查询,未命中
3) 线程2查询DB
4) 线程2根据查询的DB结果更新缓存
5) 线程1更新DB
缓存和DB数据不一致的原因:
线程1删除缓存和更新DB之间,线程2根据查询的DB结果更新了缓存,导致缓存和DB数据不一致
所有线程都是先更新DB再删除缓存,无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致
具体步骤:
1) 线程1更新DB
2) 线程2更新DB
3) 线程2删除缓存
4) 线程1删除缓存
缓存不一致原因:
无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致
在写线程更新DB再删除缓存之间,读线程可以获取到旧数据,但最终会一致
具体步骤:
1) 线程1更新DB
2) 线程2查询命中缓存返回
3) 线程1删除缓存
缓存不一致原因:
线程2获取的缓存是旧数据,但最终都会一致
先删除缓存再更新DB在并发写场景不会导致数据不一致,但是在并发读写场景会导致数据不一致
延迟双删是基于先删除缓存再更新DB的基础上的改进:在更新DB后延迟一定时间,再次删除缓存
延时是为了保证第二次删除缓存前能完成更新DB操作,延时时间根据系统的查询性能而定
第二次删除缓存是为了保证后续请求查询DB(此时数据库中的数据已是更新后的数据),重新写入缓存,保证数据一致性
无论哪个线程都会删除缓存,不会导致缓存和DB数据不一致
具体步骤:
1) 线程1删除缓存
2) 线程2删除缓存
3) 线程2更新DB
4) 线程1更新DB
5) 线程1延时删除缓存
6) 线程2延时删除缓存
缓存不一致原因:
无论哪个线程都会删除缓存,不会导致缓存和DB数据不一致
具体步骤:
1) 线程1删除缓存
2) 线程2查询,未命中
3) 线程2查询DB
4) 线程2根据查询的DB结果更新缓存
5) 线程1更新DB
6) 线程1延时删除缓存
缓存不一致原因:
线程1第一次删除缓存之后,线程2根据查询的DB结果更新缓存,此时查询得到的结果是旧数据,线程1延迟第二次删除缓存之后,后续查询DB(此时数据库中的数据已是更新后的数据),重新写入缓存,不会导致缓存和DB数据不一致
1) 需要延时,低延时场景不合适,如秒杀等需要低延时,需要强一致,高频繁修改数据场景
2) 不能保证强一致性,在更新DB之前,查询线程查询得到的结果是旧数据,可但可以减轻缓存和DB数据不一致的问题
3) 延时的时间是一个不可评估的值,延时越久,能规避一致性的概率越大
先更新DB再删除缓存在并发写场景不会导致数据不一致,但是在并发读写场景会短暂的导致数据不一致,但是由于删除缓存失败不会重试,并发写场景、并发读写场景都可能长时间导致数据不一致
异步更新缓存是基于先更新DB再删除缓存的基础上的改进:更新DB之后,基于消费队列异步删除缓存
根据消费队列不同大致分为:消息队列、binlog+消息队列
无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致
具体步骤:
1) 线程1更新DB
2) 线程2更新DB
3) 线程2把删除缓存放入消息队列
4) 线程1把删除缓存放入消息队列
5) 异步:消息队列消费删除缓存
缓存不一致原因:
无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致
异步删除缓存期间,读线程获取的缓存是旧数据,短暂出现数据不一致,异步删除缓存后最终会一致
具体步骤:
1) 线程1更新DB
2) 线程2查询缓存,命中返回
3) 线程1把删除缓存放入消息队列
4) 异步:消息队列消费删除缓存
缓存不一致原因:
异步删除缓存期间,读线程获取的缓存是旧数据,短暂出现数据不一致,异步删除缓存后最终会一致
具体步骤:
1) 线程1更新DB
2) 线程2更新DB
3) 异步:binlog日志收集中间件定时收集DB的binglog日志
4) 异步:binlog日志收集中间件发送日志消息到消息队列
5) 异步:消息队列消费删除缓存
缓存不一致原因:
无论哪个线程先更新DB再删除缓存,缓存都会被删除,不会导致缓存和DB数据不一致
具体步骤:
1) 线程1更新DB
2) 线程2查询缓存,命中返回
3) 异步:binlog日志收集中间件定时收集DB的binglog日志
4) 异步:binlog日志收集中间件发送日志消息到消息队列
5) 异步:消息队列消费删除缓存
缓存不一致原因:
异步删除缓存期间,读线程获取的缓存是旧数据,短暂出现数据不一致,异步删除缓存后最终会一致
优点
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之后,基于消费队列异步删除缓存;引入了中间件,删除缓存失败时还可以重试 |
并发读写 | 异步删除缓存期间,读线程获取的缓存是旧数据,短暂出现数据不一致,异步删除缓存后最终会一致 | 短暂不一致,最终会一致 |