Lazy loaded image
系统设计
🗒️MySQL、Redis缓存一致性问题
Words 1482Read Time 4 min
2025-8-20
2025-9-12
type
status
date
slug
summary
tags
category
icon
password
原文
在探讨这个问题是需要场景支持的,通常我们所说的缓存有本地缓存远端缓存,部署是单节点还是多节点,场景不一样遇到的挑战不一样。
首先这是个并发问题,分布式的环境下,我们很难保证请求执行的时序,在高并发的场景下通常我们追求的是最终一致性,将不一致的情况出现的概率减少。
MySQL和Redis 数据的一致性问题本质是并发、概率问题。

使用缓存的目的。

缓存是为了提升系统的性能,让客户端快速响应,因为系统访问内存的速度远比访问磁盘快很多。
但是缓存也不是没有缺点的,比如维护性,还有无法保证强一致性(CAP)。

适合使用缓存的场景:

  1. 读取的频率较高且更新频率较低的场景

不适合缓存的场景

  1. 一致性要求高的场景
  1. 读取频率低的数据
  1. 更新频率高的数据

常用的缓存更新方案

  1. 先更新库,删除缓存。
  1. 删除缓存,再更新库。

为什么不更新缓存

这里的更新缓存操作是指如下两种
  1. 先更新库,更新缓存。
  1. 更新缓存,再更新库。
notion image
描述:两个线程同时对库进行更新,由于线程2的更新缓存操作比线程1快导致线程1后续的更新缓存操作覆盖了原先缓存的值,这时就出现了缓存不一致的场景。如果之后有读请求将会读到旧值。
不推荐“更新”缓存的原由:
  • 并发更新操作,存在旧值覆盖新值的情况。通常 key 都会设置过期时间,key 的过期时间的窗口是保持一致性的机会,当然这里不是 100% 能保证。
  • 数据的更新并不意味着马上就用到,所以更新操作可能是冗余操作。
  • 减少无效的计算: 若缓存需要复杂计算(如多表查询),更新数据库后直接删除,下次读请求计算时写入,比每次写请求计算高效。

为什么不使用分布式锁来保证一致性

使用缓存是为了提高系统的性能,而采用了分布式锁,一是会让性能下降,二是添加了分布式锁又添加了新的编码和维护成本。

Cache Aside Pattern

经典的缓存一致性处理模式,本质就是先更新库,再删除缓存。这里的缓存都需要设置一个过期时间用来兜底。
Cache Aside Pattern 缓存不一致场景
Cache Aside Pattern 缓存不一致场景
 
先更新库再删除缓存,先删除缓存再更新库都会有缓存一致性问题
通常来说更新数据库的 update 操作比较耗时,大概率线程 1 的写入缓存操作会在线程 2 的更新数据库删除缓存之前或当中完成,在线程 2 的线程操作完成后,缓存已经被删除,下一个读请求进来会直接查询库中最新的数据,然后放入缓存中。
Cache Aside Pattern 理想情况
Cache Aside Pattern 理想情况
先删除缓存再更新数据库的情况下,若线程 2写入缓存操作领先线程 1 的 更新数据库操作,则后续的读请求读取的都是旧数据,需要等到缓存过期或者下一次的写请求删除缓存,且后一个读请求中的写入缓存操作发生在上一个写请求的更新数据库操作之后,这样才有可能达成缓存一致性。
notion image
上述所描述的其实都是理论情况,在并发场景中,每个线程的操作序列的排列情况可能与描述的理论情况不一样。
 

Cache Aside Pattern + 延时双删

无锁方案只是在高并发方案下的前提下尽可能减少的不一致的一种解决方案,这是AP模式模式下的一种Base技术。
notion image
描述:在更新数据库,后更新缓存的操作之上在删除操作之后再添加一个延时删除的操作,这里的延时的时间由具体的业务去决定,保证线程2的写缓存操作在线程1的两次删除操作之间。
延时时间的确定是一个难点,过短的话导致删除缓存的操作成无效操作,过长又会影响性能。

异步更新策略

MySQL、Redis 属于不同的存储组件,两者的操作都是独立的,无法保证操作 Redis、更新数据库这两个操作为原子性。异步更新策略一定程度优化了该问题(没有完全解决)。
引入了新的组件又会带来新的问题,如使用消息队列的话,要考虑消息丢失,是否要使用事务消息。所以说缓存的一致性本质上还是一个 CAP 问题,如果一致性要求高,那就使用分布式锁。如果追求性能只能选择最终一致。

总结

在高并发情况下,缓存的一致性,都是最终一致性,本质上也是一种AP模式。若想保证强一致性,需要分布式锁去做控制写缓存和删除缓存的顺序性。
 
上一篇
docker部署SpringBoot项目乱码
下一篇
SPI

Comments
Loading...