Redis 中如何保证缓存与数据库的数据一致性?

Sherwin.Wei Lv7

Redis 中如何保证缓存与数据库的数据一致性?

回答重点

缓存和数据库的同步可以通过以下几种方式:

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

2)先更新数据库存,再更新缓存

3)先删除缓存,再更新数据库,后续等查询把数据库的数据回种到缓存中

4)先更新数据库,再删除缓存,后续等查询把数据库的数据回种到缓存中

5)缓存双删策略。更新数据库之前,删除一次缓存;更新完数据库后,再进行一次延迟删除

6)使用 Binlog 异步更新缓存,监听数据库的 Binlog 变化,通过异步方式更新 Redis 缓存

以上就是实现数据库与缓存一致性的六种方式,这里前面三种都不太推荐使用,后面三种的话其主要根据实际场景:

  • 如果是要考虑实时一致性的话,先写 MySQL,再删除 Redis 应该是较为优的方案,虽然短期内数据可能不一致,不过其能尽量保证数据的一致性。
  • 如果考虑最终一致性的话,推荐的是使用 binlog + 消息队列的方式,这个方案其有重试和顺序消费,能够最大限度地保证缓存与数据库的最终一致性。
Snipaste_2024-05-18_11-11-29.jpg

扩展知识

我们以 MySQL 和 Redis 为主要实现的案例对上述几个方案再进行展开分析。

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

Snipaste_2024-05-18_11-24-36.jpg

由于网络原因,请求顺序无法保证,可能出现先更新缓存的请求,后更新数据库,而后更新缓存的请求反而先更新了数据库,这样就出现了缓存数据为 20,数据库数据为 10,即数据不一致的情况。

先写数据库,再写缓存

Snipaste_2024-05-18_16-27-02.jpg

这个问题上面其实一样,都是因为并发和网络问题导致的数据库与缓存不一致。

先删除缓存再写数据库

Snipaste_2024-05-18_16-36-07.jpg

盘一下流程:

  1. 请求 A 先对缓存中的数据进行删除操作。
  2. 请求 B 这个时候来执行查询,发现缓存中数据为空,就去数据库进行查询并回写缓存。
  3. 这个时候请求 A 删除缓存中的数据之后,进行数据库数据的更新。
  4. 但此时请求 B 已经把从数据库查询到的原始数据回写缓存了,这个时候就出现了上图的情况,数据库中查询的值是 20,而缓存中的数据是 10 。

读操作获取到的数据是过时的数据,虽然写操作已经完成了,但是因为缓存被删除了,读操作就必须从数据库中读取到旧值,并不是最新的数据。

缓存双删(先删除缓存,再写数据库,然后过一段时间再删除缓存)

Snipaste_2024-05-18_16-59-05.jpg

这个方案为了避免旧数据被回种,等待一段时间后再延迟删除缓存。

也可以使用消息队列、定时任务或者延迟任务等方式去实现延迟删除:

Snipaste_2024-05-18_17-19-34.jpg

先写数据库,再删除缓存

image.png

先把数据库的信息修改了,然后再删除对应的缓存,然后在修改数据库期间,可以允许一定时间的缓存不一致,保证缓存的最终一致性。

不过这种模型也有一定的问题,如下图所示:

Snipaste_2024-05-18_21-31-46.jpg

其主要原因在于有一个写操作,此时刚好缓存失效,又在同一时刻刚好有一个并发读请求过来,且回写缓存的请求晚于缓存删除,导致数据库与缓存的不一致。

从上面的表述可以知道,这个发生的概率比较低,一般而言业务上都会使用这个方案。

先写数据库,通过 Binlog ,异步更新缓存

Snipaste_2024-05-18_22-45-52.jpg

先修改数据库,然后通过 Canal 监听数据库的 binlog 日志,记录数据库的修改信息,然后通过消息队列异步修改缓存的数据。

这里需要注意需要保证顺序消费,保证缓存中数据按顺序更新,然后再加上重试机制,避免因为网络问题导致更新失败。

强一致性补充

可以使用分布式读写锁实现强一致性。读写锁中读和读操作不互斥,读写互斥,写写互斥。

写操作流程

  • 获取写锁。
  • 更新数据库。
  • 删除缓存。
  • 释放写锁。

读操作流程

  • 获取读锁。
  • 查询缓存:如果命中缓存,释放读锁,返回结果。如果缓存未命中,读取数据库,并将数据更新到缓存。
  • 释放读锁。
Comments