Redis 双写一致性:旁路缓存、延迟双删、读写锁、异步通知

思维导图

本文的思维导图如下:

一、什么是双写一致性?

所谓双写一致性,就是当我们修改了数据库的数据,同时也要更新缓存的数据,作到缓存与数据库的数据保持一致。

二、如何保证双写一致?

问题1:先更新数据库,还是先更新缓存?

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

比如「请求 A」和「请求 B」两个请求,同时更新「同一条」数据,可能出现这样的顺序:

A 请求先将数据库的数据更新为1,然后在更新缓存前,请求 B 将数据库的数据更新为 2,紧接着也把缓存更新为 2,然后 A 请求更新缓存为 1。

此时,数据库中的数据是 2,而缓存中的数据却是 1,出现了缓存和数据库中的数据不一致的现象

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

还是这个例子,「请求 A」和「请求 B」两个请求,同时更新「同一条」数据,可能出现这样的顺序:

A 请求先将缓存的数据更新为 1,然后在更新数据库前,B 请求来了,将缓存的数据更新为 2,紧接着把数据库更新为 2,然后 A 请求将数据库的数据更新为 1。

此时,数据库中的数据是1,而缓存中的数据却是 2,出现了缓存和数据库中的数据不一致的现象。


所以,无论是先更新数据库,还是先更新缓存,都会存在并发问题。

当两个请求并发地更新同一条数据的时候,可能会出现数据不一致的现象。

解决方案:旁路缓存策略

为了解决上面的问题,我们在更新数据时,不更新缓存,而是删除缓存中的数据。

然后,到读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。

这个策略就是 Cache Aside 策略 ,中文是旁路缓存策略

该策略又可以细分为「读策略」和「写策略」。

写策略

● 更新数据库中的数据

● 删除缓存中的数据

读策略

● 如果读取的数据命中了缓存,则直接返回数据

● 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。

在「写策略」的时候,该选择哪种顺序呢?

● 先删除缓存,再更新数据库;

● 先更新数据库,再删除缓存。

问题2:先更新数据库,还是先删除缓存?

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

正常情况 2. 异常情况

正常情况:

初始情况:

● 缓存:10

● 数据库:10

线程2:

1.把数据库的 10 更新为 20

2.删除缓存的 10

线程1:

  1. 查询缓存的 10,未命中 → 查询数据库,查到数据库的 20

  2. 将数据库的 20 写入缓存


最终结果:

● 缓存:20

● 数据库:20


但是,因为线程是可以交替进行的,所以我们再看异常情况:

初始情况:

● 缓存:10

● 数据库:10


线程1:

1.查询缓存的 10,未命中 → 查询数据库,查到数据库的 10

线程2:

  1. 把数据库的 10 更新为 20

  2. 删除缓存的 10
    线程1:

  3. 将数据库的 10 写入缓存


最终结果:

● 缓存:10

● 数据库:20

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

正常情况 异常情况

正常情况:

初始情况:

● 缓存:10

● 数据库:10


线程1:

1.删除缓存的 10

2.更新数据库为 20

线程2:

  1. 查询缓存的 10,未命中

  2. 查到数据库的 20,并将其写入缓存


最终结果:

● 缓存:20

● 数据库:20

但是,因为线程是可以交替进行的,所以我们再看异常情况。

初始情况:

● 缓存:10

● 数据库:10


线程1:

1.删除缓存的 10

线程2:

  1. 查询缓存的 10,未命中

  2. 查到数据库的 10 → 并将其写入缓存

线程1:

  1. 在不知道刚刚删除的缓存的 10 被线程 2 恢复的情况下,更新数据库为 20

线程 1 的内心:你个老 6,我刚删的 10,你怎么又去写上了?!


最终结果:

● 缓存:10

● 数据库:20

结论: 无论是先更新数据库,还是先删除缓存,都会存在脏读。

当两个请求并发地更新同一条数据的时候,可能会出现数据不一致的现象。

解决方案:延时双删

读操作
  1. 客户端发送请求要查询数据
  2. 在 Redis 里查询数据:
    1. 命中,就直接返回结果;
    2. 未命中,再查询数据库 → 数据库查询到结果 → 把数据写入Redis → 把数据返回给客户
写操作:延迟双删
  1. 客户端发送请求要更新数据
  2. 先删除缓存 → 修改数据库中的数据 → 过一会儿再删除缓存

在数据更新时,先更新数据库,还是先更新缓存?

为什么要删除两次缓存呢?

为什么要延时删除?

问题3:为什么要删除两次缓存?

因为无论是先删除缓存,还是先修改数据库,都会存在脏读情况。

所以我们要删除两次缓存:

  • 第一次删:防止写期间命中脏缓存。
  • 第二次删:干掉「读线程在写窗口内回填的旧值」。

问题4:为什么要延时删除?

因为我们的数据库可能是主从架构的,需要等待一会让两个数据库同步。

但这个延迟时间毕竟不好控制,所以这种方案还是有脏数据的风险。

增强手段

读写锁

代码实现

Redisson已经为我们提供了读写锁。

读操作:

写操作:

优点: 强一致

缺点: 低性能

异步通知

在允许稍微延迟的业务情况下,我们会通过异步通知来保证数据的最终一致性。

基于 MQ

我们可以基于 MQ 中间件来实现异步通知:

基于 Canal

我们还可以基于 Canal 中间件来实现异步通知:

Canal 伪装成 MySQL 的从节点,来读取 BINLOG。

二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句。

三、相关面试题

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

等价问题:Redis 作为缓存,MySQL 的数据如何与 Redis 进行同步?

回答这个问题一定要结合自身业务前提,首先应该要介绍自己的业务背景。

1)业务允许延迟一致性又是另一种方案

【介绍自己简历上的业务】允许延迟的业务:

嗯,就说我最近做的这个项目,++我们当时是把文章的热点数据存入到了缓存中++,虽然是热点数据,但是实时要求性并没有那么高。所以,我们当时采用的是异步的方案同步的数据

【介绍你的方案】介绍 Redisson 读写锁的方案:

允许延时一致的业务,采用异步通知:

  1. 我们使用 MQ 中间件,更新数据之后,通知缓存删除。
  2. 我们使用的是阿里的 canal 中间件,不需要修改业务代码,部署一个 canal 服务,伪装成 mysql 的一个从节点,当 mysql 数据更新以后,canal 会通过读取 binlog 数据更新缓存

2)业务一致性要求高是一种方案

【介绍自己简历上的业务】要求强一致的业务:

嗯,就说我最近做的这个项目,++我们当时是把抢券的库存存入到了缓存中++,这个需要数据库与 Redis 保持高度一致。

为了保证数据的强一致性,我们当时采用的是 Redisson 提供的读写锁来保证数据的同步。

【介绍你的方案】介绍 Redisson 读写锁的方案:

  1. 在读的时候添加共享锁:
    1. 可以保证读读不互斥,读写互斥。
    2. 加锁之后,其他线程可以共享读操作
  2. 在跟新数据的时候,添加排他锁:
    1. 它是读写、读读都排斥
    2. 加锁之后,阻塞其他线程读写操作
    3. 这样就能保证在写数据的同时不会让其他线程读数据,从而避免了仓数据。

追问:排他锁是如何保证读写、读读互斥的呢?

回答:其他排他锁底层使用也是 setnx,保证了同一时刻只能有一个线程操作锁住的方法。

追问:为什么不用延时双删?

回答:延时双删。如果时写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不好缺点,且再延时的过程中也可能会出现脏数据,并不能保证强一致性,所以没有采用它。

相关推荐
一瓢西湖水7 小时前
列式数据库-以clickHouse为例
数据库·clickhouse
Elastic 中国社区官方博客7 小时前
使用 Elastic Cloud Serverless 扩展批量索引
大数据·运维·数据库·elasticsearch·搜索引擎·云原生·serverless
liulanba7 小时前
AI Agent技术完整指南 第一部分:基础理论
数据库·人工智能·oracle
没有bug.的程序员7 小时前
服务安全:内部服务如何防止“裸奔”?
java·网络安全·云原生安全·服务安全·零信任架构·微服务安全·内部鉴权
逆天小北鼻7 小时前
Oracle 服务端与客户端的核心区分要点
数据库·oracle
2501_946242937 小时前
MPV-EASY Player (MPV播放器) v0.41.0.1
数据库·经验分享·云计算·计算机外设·github·电脑·csdn开发云
一线大码7 小时前
SpringBoot 3 和 4 的版本新特性和升级要点
java·spring boot·后端
哈里谢顿7 小时前
redis常见问题分析
redis
weixin_440730508 小时前
java数组整理笔记
java·开发语言·笔记
weixin_425023008 小时前
Spring Boot 实用核心技巧汇总:日期格式化、线程管控、MCP服务、AOP进阶等
java·spring boot·后端