Redis——双写一致性

文章目录

  • [1. 问题介绍](#1. 问题介绍)
    • [1.1 定义](#1.1 定义)
    • [1.2 起因](#1.2 起因)
  • [2. 解决方案](#2. 解决方案)
    • [2.1 方案一:延迟双删](#2.1 方案一:延迟双删)
      • [2.1.1 思想](#2.1.1 思想)
      • [2.1.2 实现方式](#2.1.2 实现方式)
      • [2.1.3 存在的问题](#2.1.3 存在的问题)
      • [2.1.4 优点](#2.1.4 优点)
      • [2.1.5 缺点](#2.1.5 缺点)
      • [2.1.6 适用场景](#2.1.6 适用场景)
    • [2.2 方案二:使用 Canal 监听 MySQL 从节点的数据变化](#2.2 方案二:使用 Canal 监听 MySQL 从节点的数据变化)
      • [2.2.1 介绍](#2.2.1 介绍)
      • [2.2.2 工作原理](#2.2.2 工作原理)
      • [2.2.3 解决双写不一致的思想](#2.2.3 解决双写不一致的思想)
      • [2.2.4 优点](#2.2.4 优点)
      • [2.2.5 缺点](#2.2.5 缺点)
      • [2.2.6 适用场景](#2.2.6 适用场景)
    • [2.3 方案三:读写锁](#2.3 方案三:读写锁)
      • [2.3.1 思想](#2.3.1 思想)
      • [2.3.2 做法](#2.3.2 做法)
      • [2.3.3 优点](#2.3.3 优点)
      • [2.3.4 缺点](#2.3.4 缺点)
      • [2.3.5 适用场景](#2.3.5 适用场景)
  • [3. 总结](#3. 总结)

1. 问题介绍

1.1 定义

在 Redis 中,双写一致性 指的是当数据更新时,既要更新数据库中的数据,又要更新 Redis 中的数据,并且要保证这两份数据的一致性。相对而言,双写不一致 就指的是数据更新后,数据库中的数据和缓存中的数据不一致。

1.2 起因

更新操作 和 查询操作 可能是并发的,从而导致在更新操作删除 Redis 的旧数据之后,查询操作再次将旧数据缓存到 Redis 中,从而造成两份数据不一致。

假设查询的流程如下:
Created with Raphaël 2.3.0 开始 从 Redis 中读取 Redis 中的数据是否为 null 从数据库中读取 数据库中的数据是否为 null 返回数据 结束 将数据库中的数据缓存到 Redis 中 yes no yes no

更新有两种方案:

  • 第一种方案:先删除缓存,再更新数据库。
  • 第二种方案:先更新数据库,再删除缓存。

对于 查询 和 更新方案一 的并发执行,如果按照如下的时序图,则会缓存旧数据:
更新操作 数据库 Redis 查询操作 删除缓存 查询缓存 发现缓存为 null,查询数据库 由于某些操作浪费了一些时间 更新数据库 缓存旧数据 更新操作 数据库 Redis 查询操作

对于 查询 和 更新方案二 的并发执行,如果按照如下的时序图,则会缓存旧数据:
更新操作 数据库 Redis 查询操作 查询缓存 发现 缓存为 null,查询数据库 更新数据库 删除缓存 由于某些操作浪费了一些时间 缓存旧数据 更新操作 数据库 Redis 查询操作

上面这两种情况在生产中 都有可能发生,双写一致性就是要避免这个问题。

2. 解决方案

2.1 方案一:延迟双删

2.1.1 思想

如果更新操作在完成更新数据库和删除缓存 之后再删除一遍缓存 ,那么就能解决这个问题,从而得出 延迟双删 的解决方案:在更新操作的最后一步执行延迟删除缓存的操作。

2.1.2 实现方式

  • 通过 ScheduledExecutorServiceschedule() 实现:在更新操作的末尾,使用 ScheduledExecutorServiceschedule(Runnable command, long delay, TimeUnit unit) 方法,指定时间延迟删除 Redis 中的缓存。
  • 通过消息队列的延迟消息实现:在更新操作的末尾,生产一条删除 Redis 中指定缓存的延迟消息,然后让消费者去消费这条消息,删除 Redis 中指定的缓存。

2.1.3 存在的问题

1s 之后再次删除可以避免绝大多数双写不一致问题,因为很少有查询操作的时间会超过 1s。但由于生产中的 MySQL 往往是以集群模式部署的,会有 主从同步 的时间消耗,如果在从节点没有更新数据之前执行查询操作,就会读到旧数据,这时可以相对增加一点延迟时间,比如延迟 3s 后再次删除。所以 延迟双删有双写不一致的风险

2.1.4 优点

  • 性能高:由于读和写是并发的,所以性能会很高。
  • 实现比较简单:由于可以使用定时任务实现,所以实现比较简单。
  • 保证了数据的最终一致性:由于延迟删除缓存,所以缓存中的数据和数据库中的数据最终是一致的。

2.1.5 缺点

  • 无法保证数据的强一致性:由于 延迟删除缓存的时刻 可能与 数据更新完毕(主从同步之后)的时刻 间隔了不少时间,在这期间数据的一致性无法保障。

2.1.6 适用场景

本方案适用于 允许数据短暂不一致、对性能要求较高 的场景,大多数生产场景都是如此,比如文章浏览量的更新。

2.2 方案二:使用 Canal 监听 MySQL 从节点的数据变化

2.2.1 介绍

Canal 是阿里巴巴开源的一个基于 MySQL 数据库增量日志解析的中间件,它提供了增量数据订阅和消费的功能,主要用于捕获数据库数据变更,将其发送给其他系统进行处理。

2.2.2 工作原理

类似于 MySQL 的 主从复制 机制:将主数据库的 binlog (二进制日志) 传输到从数据库,从数据库根据 binlog 修改数据,从而实现数据的同步。

Canal 也是解析 MySQL 的 binlog,不过它不是用于数据同步到另一个数据库,而是把变更数据以消息的形式发送给下游的应用程序。

2.2.3 解决双写不一致的思想

当 MySQL 从节点中的数据被更新时,Canal 通知 Redis 删除缓存。

2.2.4 优点

  • 无代码侵入性:其它方案多少都需要在更新操作中添加代码,而使用本方案无需在更新操作中添加代码。
  • 数据的一致性相较方案一更强:当 MySQL 从节点中的数据被更新时,Canal 通知 Redis 删除缓存,这样依赖,本方案的删除时刻 就会比 方案一中延迟删除时刻 早一点,删除时刻 是 数据更新完毕(主从同步之后)的时刻。
  • 数据库的压力小:Canal 通过直接解析 MySQL 的 binlog 文件来获取数据变更,避免了频繁地查询数据库表,减少了对数据库的压力。

2.2.5 缺点

  • 数据一致性仍不是很强:虽然本方案对比方案一提升了数据的一致性,但在 主节点修改数据 到 从节点同步数据 的时间段内,数据仍是不一致的。
  • 配置和管理复杂:Canal 的配置相对复杂,需要对 MySQL 的 binlog 配置、Canal 自身的服务器配置、客户端配置等多个方面进行正确设置。

2.2.6 适用场景

本方案也适用于 允许数据短暂不一致、对性能要求较高 的场景。

2.3 方案三:读写锁

2.3.1 思想

既然查询和更新操作并发会影响数据的一致性,那么直接禁止查询和更新操作并发即可,这时就可以给查询操作加上 读锁 ,给更新操作加上 写锁

以下是读锁和写锁的特性:

  • 读锁:共享锁,只会与排他锁发生排斥,与共享锁不会发生排斥。
  • 写锁:排他锁,与所有锁发生排斥。

它们之间的排斥关系如下表所示:

排斥关系 读锁 写锁
读锁 不排斥 排斥
写锁 排斥 排斥

这样一来,查询操作可以并发,但会被更新操作阻塞,从而避免了双写不一致的问题。

2.3.2 做法

使用 Redisson 框架提供的读写锁,代码如下所示:

java 复制代码
RReadWriteLock rwLock = redisson.getReadWriteLock("lock");	// 获取读写锁
RLock readLock = rwLock.readLock();							// 从读写锁中获取读锁
readLock.lock();											// 使用读锁
try {
    // 执行查询操作
} finally {
    readLock.unlock();										// 释放读锁
}
java 复制代码
RReadWriteLock rwLock = redisson.getReadWriteLock("lock");	// 获取读写锁
RLock writeLock = rwLock.writeLock();						// 从读写锁中获取写锁
writeLock.lock();											// 使用写锁
try {
    // 执行更新操作
} finally {
    writeLock.unlock();										// 释放写锁
}

注意:获取读锁和写锁之前都需要先获取读写锁,而且读写锁的键必须一致。

2.3.3 优点

  • 保证了数据的强一致性:查询操作和更新操作不是并发的,从根源上避免了双写不一致的问题。

2.3.4 缺点

  • 性能相对较差:由于查询操作和更新操作是相互阻塞的,但查询操作却是可以并发的,所以性能相对较差。
  • 对代码的侵入性比较大:相对于方案二 (无侵入) 和方案一 (只在更新操作的末尾加了一段代码),本方案要求在查询时获取读锁,在更新时获取写锁,对代码的侵入性比较大。

2.3.5 适用场景

本方案适用于 允许性能不是很高、要求数据强一致 的场景,尤其是与钱相关的业务。

3. 总结

Redis 中,双写不一致问题发生在 查询操作 和 更新操作 并发的时候,当更新操作只删除一次缓存时,查询操作可能会把旧数据缓存起来,从而导致双写不一致。

解决方案主要有三种:

  • 延迟双删:在删除一遍缓存后,间隔一段时间再次删除缓存。两次删除间隔的时间段内,查询到的所有数据都是旧数据。
  • 使用 Canal 监听 MySQL 从节点的数据变化:使用阿里巴巴开发的 Canal 中间件,监听 MySQL 从节点的数据变化,变化之后通知 Redis 删除数据。在 主节点更新数据 到 从节点同步数据后通知 Redis 删除数据 的时间段内,查询到的所有数据都是旧数据。
  • 读写锁:给查询操作加上读锁,给更新操作加上写锁,从根源上避免读写并发问题。保证了数据的强一致性,但相对前两种方案,性能比较低。
相关推荐
Q_19284999062 分钟前
基于Spring Boot的电影售票系统
java·spring boot·后端
我要学编程(ಥ_ಥ)1 小时前
初始JavaEE篇 —— 网络原理---传输层协议:深入理解UDP/TCP
java·网络·tcp/ip·udp·java-ee
就爱学编程1 小时前
重生之我在异世界学编程之C语言:数据在内存中的存储篇(下)
java·服务器·c语言
yuanbenshidiaos1 小时前
C++--------------树
java·数据库·c++
俎树振1 小时前
Java数组深入解析:定义、操作、常见问题与高频练习
java·开发语言
花心蝴蝶.2 小时前
Map接口 及其 实现类(HashMap, TreeMap)
java·数据结构
小天努力学java2 小时前
【面试系列】深入浅出 Spring
java·spring·面试
Just_Paranoid2 小时前
解析 Java 项目生成常量、变量和函数 Excel 文档
java·python·正则表达式·excel·javadoc
阿垂爱挖掘2 小时前
34 - Java 8 Stream
java
程序员JerrySUN2 小时前
Yocto 项目 - 共享状态缓存 (Shared State Cache) 机制
linux·嵌入式硬件·物联网·缓存·系统架构