redis深入学习二

1、RDB持久化和恢复机制

RDB(Redis DataBase) 是Redis默认的持久化方式,它在指定时间间隔内将内存中的数据集以二进制快照(Snapshot)形式写入磁盘,生成一个经过压缩的.rdb文件。

1、生成机制:如何创建快照

RDB文件的生成有两种方式:手动触发和自动触发,但核心的执行逻辑都由bgsave命令完成。

java 复制代码
# 1. 自动触发(配置文件)
save 900 1     # 900秒(15分钟)内至少有1个key被改动
save 300 10    # 300秒(5分钟)内至少有10个key被改动
save 60 10000  # 60秒(1分钟)内至少有10000个key被改动

# 2. 手动触发
redis-cli save      # 同步保存(阻塞主进程)
redis-cli bgsave    # 异步保存(fork子进程)

# 3. 关闭时触发
shutdown save       # 关闭时保存RDB

手动触发命令:

  • save:在主进程中同步执行,会阻塞所有客户端请求,直到快照生成完毕。生产环境基本不用。
  • bgsave:在后台异步执行。主进程会fork一个子进程来处理所有繁重的写入工作,自己则继续响应客户端,这是RDB生成的主要方式。

自动触发条件:

  • 通过配置文件redis.conf中的save选项来设定,满足条件时Redis会自动执行一次bgsave。例如,Redis默认配置为:

第一步:触发与前置检查

RDB快照的生成由手动执行bgsave命令或自动满足save配置条件触发。触发后,主进程首先检查是否有bgsave或bgrewriteaof子进程正在运行,若有则直接放弃本次执行,若无则进入下一步。

第二步:fork子进程

主进程调用fork()系统调用创建子进程,此时主进程会短暂阻塞(毫秒级)。fork完成后,主进程立即恢复并继续正常处理所有客户端请求,父子进程共享同一份物理内存。

第三步:子进程写入RDB文件

子进程打开临时文件temp-.rdb,按RDB格式依次写入文件头("REDIS"加版本号)、元数据(如创建时间等)、各数据库的所有键值对(包括键、值和过期时间),最后在文件尾部写入EOF标记和CRC64校验和。写入完成后关闭临时文件并通知主进程。

第四步:主进程并行处理与写时复制

在子进程生成快照的同时,主进程继续正常处理所有读写请求。若主进程修改数据时发现该内存页正被子进程引用,则触发写时复制(Copy-On-Write)机制,复制该内存页并在新页上执行修改,而子进程仍读取旧页,从而保证快照数据的一致性。

第五步:原子替换与收尾

主进程收到子进程的完成通知后,执行rename()将临时文件原子替换为正式的RDB文件(如dump.rdb)。最后子进程退出并回收资源,主进程记录日志,本次RDB生成完成。

第六步:关键结论

RDB文件记录的是fork那一刻的内存快照,fork之后主进程的所有新写入均不落入本次RDB文件,只能等待下一次触发时才会被持久化。

总结

RDB 快照生成 = 主进程 fork 子进程 + 子进程读取 fork 时刻的内存快照写入临时文件 + 主进程继续处理请求(修改时 COW) + 最后原子替换文件

2、加载机制:如何加载快照

RDB 快照恢复加载"的所有场景

场景 3(主从同步):这是从节点的行为。当从节点发现自己与主节点数据差异太大时,会清空自己全部数据,然后接收主节点发来的整个 RDB 文件并加载,这个加载过程会阻塞从节点,期间无法响应读请求。

场景 4(故障转移):这是集群/哨兵的行为。当主节点宕机后,一个从节点会被选举为新的主节点。这个"新主节点"在升主那一刻,会加载自己磁盘上的 RDB 文件(或内存中已有的数据)来提供服务。它并不是从旧主节点那里复制 RDB,而是使用自己已有的数据。

java 复制代码
只要是加载 RDB(无论是从磁盘文件还是网络流),Redis 的主进程都会同步阻塞,直到数据全部加载完毕才能继续处理命令。这是所有场景的共同底层机制。

恢复加载过程流程图

第一步:启动时选择数据源

Redis服务器启动时,首先检查AOF持久化功能是否开启。由于AOF通常比RDB记录的数据更完整(丢失更少),如果AOF功能开启,Redis会优先使用AOF文件来恢复数据,完全跳过RDB加载流程。只有在AOF关闭的情况下,Redis才会尝试加载RDB文件。

第二步:检查RDB文件是否存在

确认使用RDB恢复后,Redis会在工作目录下查找配置文件中指定的RDB文件(默认名为dump.rdb)。如果文件不存在,则不做任何恢复,直接启动一个空数据库实例;如果文件存在,则进入加载流程。

第三步:打开并校验文件

Redis以只读方式打开RDB文件,首先读取文件头部5字节的魔数"REDIS",校验其是否为合法的RDB文件。然后读取4字节的版本号,用于判断文件格式的兼容性。如果版本过高或魔数不匹配,Redis会报错并拒绝启动。

第四步:读取元数据与数据内容

校验通过后,Redis开始顺序读取文件中的各个部分,依次解析元数据(如创建时间、Redis版本等)、数据库编号(SELECTDB操作符)以及各数据库中的所有键值对。读取过程中,Redis会直接将这些数据在内存中重建,恢复到对应的数据库和键空间。

第五步:校验文件完整性

数据读取完成后,Redis读取文件尾部的CRC64校验和,对文件内容重新计算校验和并进行比对。如果校验和一致,说明文件完整有效,加载成功;如果不一致,说明文件在磁盘上已损坏,Redis会报错并拒绝启动,防止使用损坏的数据。

第六步:加载完成与阻塞说明

在整个加载过程中,Redis服务器处于阻塞状态,不处理任何客户端请求,直到全部数据加载完毕并完成校验后,Redis才正式启动,开始接收并处理外部连接。

第七步:关键结论

RDB加载是自动的,在服务器启动时无条件执行(前提是AOF关闭且RDB文件存在),且加载期间服务器完全不可用。如果RDB文件损坏,Redis会拒绝启动,因此日常运维中需要配合RDB文件的备份和校验机制来保证数据可恢复性。

2、AOF 持久化和恢复机制

Redis 的 AOF(Append Only File)持久化机制,核心思想是通过记录并重新执行所有写命令来恢复数据。它与 RDB 保存某一时刻的数据快照不同,AOF 更像一个"操作日志",因此能够提供更实时的数据安全保障。

1、生成机制:如何创建

第一步------写命令追加

AOF的生成并非像RDB那样由子进程生成快照文件,而是主进程在每次执行完一个写命令(如SET、HSET、LPUSH等)后,立即将该命令以Redis协议的文本格式(即RESP协议)追加到内存缓冲区(aof_buf)的末尾。这个阶段只是将命令写入内存缓冲区,并未真正落到磁盘。

生成机制:第二步------缓冲区同步到磁盘(刷盘策略)

Redis主循环中会周期性调用flushAppendOnlyFile函数,根据配置文件中的appendfsync选项决定何时将缓冲区内容写入并同步到磁盘上的AOF文件。appendfsync有三种策略:always表示每次事件循环都将缓冲区写入并同步到磁盘,性能最低但最安全,最多丢失一个事件循环中的数据;everysec表示每秒执行一次同步,性能与安全性折中,最多丢失一秒内的数据,这是默认策略;no表示只写入操作系统页缓存,由操作系统决定何时同步到磁盘,性能最高但最不安全,宕机时可能丢失大量数据。

生成机制:第三步------AOF文件持续增长

随着Redis持续运行,所有写命令会不断追加到AOF文件中,导致AOF文件体积越来越大。当文件体积膨胀到一定程度时,Redis会触发AOF重写机制(bgrewriteaof)来压缩文件体积。

生成机制:第四步------AOF重写(文件压缩)

AOF重写并非对旧AOF文件的修改或整理,而是由主进程fork一个子进程,子进程读取当前内存中的全部数据,将其转换为一系列重建数据所需的最小写命令集合,写入一个新的临时AOF文件。例如,内存中有key=name,先后执行了SET name a、SET name b、SET name c三条命令,原始AOF文件记录了三条命令,而重写后的新文件只记录一条SET name c即可恢复当前状态,从而大幅压缩文件体积。重写期间,主进程继续处理新请求,并将新请求同时写入旧的aof_buf和专门的重写缓冲区(rewrite buffer),保证新旧文件的命令不丢失。子进程完成后,主进程将重写缓冲区中的增量命令追加到新文件尾部,然后原子替换旧AOF文件。

生成机制:第五步------关键结论

AOF生成本质是实时记录写命令,通过appendfsync策略平衡性能与安全性;AOF重写通过fork子进程压缩文件体积,重写期间的新请求通过双缓冲区机制保证不丢失。

2、恢复机制

当 Redis 重启并开启了 AOF 功能时,它的数据恢复流程与 RDB 有本质区别:

优先加载:如果配置了 appendonly yes,Redis 在启动时会优先选择加载 AOF 文件来恢复数据,而忽略 RDB 文件,因为 AOF 通常拥有更完整、更新的数据。

模拟客户端重放:Redis 会创建一个没有网络连接的"伪客户端"(fake client)。

逐一执行命令:从 AOF 文件中按 RESP 协议格式分析并读出一条条写命令,然后让"伪客户端"逐一执行这些命令,将数据"重放"回内存中,直到所有命令执行完毕。

2、Redis存在线程安全问题吗

关于Redis的线程安全问题,需要从"Redis服务器内部"和"多个客户端使用Redis"两个层面来区分看待,这两者的结论不同。

Redis服务器内部:是线程安全的

Redis的核心命令执行是单线程模型,这保证了其内部的线程安全。

核心机制:Redis使用一个主线程顺序、串行地处理所有客户端命令。多个请求到达后会进入队列排队,依次执行,不存在多线程同时修改同一份数据的内部冲突。

为何高效:单线程模型让Redis内部实现简单,无需加锁,避免了线程切换和竞争的开销。同时,它通过IO多路复用技术高效处理网络请求,并且所有操作都在内存中进行,速度极快。

版本演进:Redis 6.0引入了多线程IO,但仅用于网络数据的读写和解析,核心的命令执行依然是单线程,因此不会引入新的线程安全问题。

多个客户端使用:会产生"线程安全"类问题

当多个客户端(可以类比为多线程)同时操作同一个共享数据时,就会出现类似线程安全的问题。

问题根源:问题并非出自Redis内部,而是源于多个客户端并发操作缺乏协调。即使每个Redis命令是原子的,如果业务由多个命令组合而成(如先读取、再判断、最后写入),就不是原子操作,并发时就会出错。

解决方案:保证复合操作的原子性

解决这类并发问题,核心是保证一组相关操作的原子性,Redis提供了几种方案:

  • 使用Lua脚本:将多个命令封装成一个脚本整体执行,期间不会被其他命令插入,保证原子性。

  • 使用Redis事务(MULTI/EXEC):将一组命令放入事务队列,然后一次性按顺序执行。

  • 使用原子复合命令:直接使用Redis提供的原子命令,如 SETNX(原子地设置不存在的键)或 MSET(原子地设置多个键)。

  • 使用分布式锁:在应用层引入分布式锁(如Redlock),确保同一时刻只有一个客户端能操作特定资源

总结

  • 单条命令/服务器内部:是安全的。得益于其核心命令处理的单线程模型。

  • 多客户端组合操作:不安全。需要开发者通过Lua脚本、事务或分布式锁等方式,自行保证业务逻辑的原子性

3、 Redis 6.0 多线程及为什么使用多线程,会有线程安全问题吗

什么引入多线程?------ 突破网络 I/O 瓶颈

Redis 此前一直是单线程模型,因为其性能瓶颈通常不在 CPU,而在内存和网络。但互联网的高速发展带来了新的挑战:

  • 多核 CPU 利用率不足:现代服务器普遍是多核 CPU,但单线程的 Redis 只能使用一个核心,无法充分释放硬件潜力 。

  • 网络 I/O 成为瓶颈:在高并发场景下(如数万连接、大数据包),网络数据的读写(read/write 系统调用)会耗费大量 CPU 时间。此时,单线程既要处理网络 I/O,又要执行命令,就会成为瓶颈,导致吞吐量增长停滞,延迟增加 。

正是为了解决这个问题,Redis 6.0 引入了多线程,将网络 I/O 的读写操作分配给多个线程并行处理,从而实现了性能的大幅提升。有测试表明,启用多线程 I/O 后,Redis 的吞吐量可以提升一倍甚至更多

多线程安全的

Redis 6.0 的多线程模型是高度谨慎和专门化的,它的核心原则是:仅将网络 I/O(读写)并行化,而所有命令的执行(操作内存数据)仍然由主线程串行处理

3、Redis主从复制的原理

4、Redis哨兵模式的原理

5、Redis集群模式的原理

6、 Redis 分布式锁及使用场景

核心原理

  • 互斥性(加锁的原子性)

    原理:利用Redis单线程处理命令的特性,通过SET key value NX PX这一条原子命令来完成"检查是否存在"和"设置值"两个动作。如果NX(只在Key不存在时设置)成功,说明当前客户端抢到了锁。

    关键点:value必须是全局唯一的(如UUID或机器ID+线程ID),这是后续安全解锁的基础。

  • 防死锁(TTL自动释放)

    原理:PX参数给锁设置了过期时间(TTL)。即使客户端宕机或网络中断,锁也会在TTL后自动被Redis删除,避免锁永远被持有,导致整个系统死锁。

    关键点:过期时间设置需要合理------太短业务没执行完锁就没了,太长会影响故障恢复速度。

  • 安全解锁(Lua原子操作)

    原理:解锁时,不能直接用DEL,因为可能误删别人的锁(比如自己持有锁时业务超时,锁自动释放了,别人拿到锁后,自己才执行DEL,就会删掉别人的锁)。必须先用GET判断值是否等于自己的unique_value,再决定是否DEL。

    关键点:这两个操作必须封装在Lua脚本中,提交给Redis一次性执行,保证原子性。

完整执行过程(从加锁到解锁的生命周期)

总结:

  • Redis分布式锁的本质是用Redis的原子性操作模拟一个"全局信号量",通过TTL防止死锁,通过唯一值防止误删。
    它的核心价值是降低并发冲突,而不是提升并发性能。使用时必须明确:锁的粒度、超时时间、以及是否真的需要锁来解决这个业务问题。

7、Redisson原理

Redisson 是一个基于 Redis 构建的 Java 分布式服务框架,它的原理可以理解为:在 Redis 这个高性能的键值数据库之上,用 Java 熟悉的 API 包装出了一整套分布式工具。它让我们操作分布式缓存和实现分布式锁,就像操作本地的 HashMap 和 ReentrantLock 一样自然

通信与数据交互的基石

  • 性能网络通信:Redisson 底层基于 Netty 框架 实现异步非阻塞通信,能高效处理大量并发连接。所有操作在底层都是异步的,返回一个 RFuture 对象,你可以选择同步等待,也可以添加回调函数异步处理,这为高并发场景打下了基础。

  • 连接管理:它通过 ConnectionManager 统一管理连接池,支持单机、哨兵、集群等多种Redis部署模式,并负责连接的生命周期、心跳检测和自动重连。

  • 灵活的编解码:Redisson 使用可插拔的编解码器来序列化Java对象。默认是 JacksonJsonCodec,你也可以根据需要换成其他方式,这保证了数据存储的灵活性

把 Redis 数据结构"变成"Java对象

分布式锁的精妙实现:以可重入锁为例

这是 Redisson 最核心的能力之一。它实现的 RLock 接口继承了 Java 标准的 java.util.concurrent.locks.Lock 接口。其核心实现原理如下

  • 加锁的原子性保障(Lua脚本):加锁和解锁操作都是通过执行一段 Lua 脚本 完成的。Lua 脚本在 Redis 中是原子执行的,这从根本上保证了分布式锁的互斥性。

  • 数据结构与可重入性:锁在 Redis 中是用 Hash 数据结构存储的。Key 是锁的名称,Field 是持有锁的客户端ID+线程ID(唯一值),Value 是重入次数。当同一个线程再次请求锁时,脚本会检查 Field 是否存在,如果存在则让 Value 加1,实现锁的重入。这也是 RLock 为什么支持可重入的原因。

  • 看门狗(Watchdog)自动续期:这是 Redisson 解决"锁提前释放"问题的核心机制。当调用无参的 lock() 方法时,会触发看门狗。

  • 核心源码逻辑:加锁时,如果未指定 leaseTime,会使用默认的看门狗超时时间(默认30秒)。成功后,会调用 scheduleExpirationRenewal 方法,它会在一个内部 Map(EXPIRATION_RENEWAL_MAP)中记录该锁的续期条目,并启动一个定时任务。

  • 定时续期任务:这个定时任务会每隔 internalLockLeaseTime / 3(默认10秒)执行一次。其核心是通过 Lua 脚本 renewExpirationAsync 检查锁是否仍被当前线程持有,如果是,就执行 pexpire 命令将锁的过期时间重置为30秒。

  • 任务清理:当业务线程调用 unlock() 释放锁时,会调用 cancelExpirationRenewal 方法,从 EXPIRATION_RENEWAL_MAP 中移除对应的续期任务,停止看门狗。

  • 公平锁的实现:Redisson 也提供了 RedissonFairLock。其公平性通过 Lua 脚本 和 Redis 的 Sorted Set(有序集合) 来实现。加锁时,线程会先尝试获取锁,失败后则按时间戳将自身信息加入等待队列(Sorted Set),并只有队首的线程才有资格去竞争锁,从而保证了"先来先得"

总结

  • Redisson 的原理精髓在于,它不仅仅是一个 Redis 客户端,而是一个在 Redis 之上构建的、易用的分布式工具集。它通过 Netty 实现高性能通信,通过 Lua 脚本保证原子操作,通过将 Redis 指令封装成 Java 接口提升开发体验。其中,可重入锁配合看门狗自动续期的机制,是其最经典、最广泛使用的功能,为分布式环境下的并发控制提供了一个优雅而强大的解决方案。

8、看门狗

看门狗的本质是:在锁被持有期间,通过一个后台定时任务,周期性地重置锁的过期时间。它的实现涉及加锁时的初始化、定时续期的核心逻辑,以及解锁时的清理三个关键环节。

整个过程分为 4 个阶段:

  • 阶段① 加锁 & 启动(业务线程发起)

    1、调用 lock()(未传过期时间)。

    2、Redisson 通过 Lua 脚本在 Redis 中写入锁 Key,初始 TTL = 30 秒。

    3、加锁成功后,立即启动一个后台定时任务(看门狗),该任务与当前线程绑定。

  • 阶段② 定时续期(后台线程循环)

    1、看门狗每隔 TTL / 3 = 10 秒 执行一次。

    2、每次执行:

    检查 Redis 中该锁的 Value 是否仍为当前线程标识。

    若是,执行 Lua 脚本将锁的 TTL 重置为 30 秒(续命)。

    若否(锁已丢失或已释放),立即取消该定时任务。

  • 阶段③ 业务完成 & 主动释放(业务线程发起)

    1、业务代码执行完毕,在 finally 块中调用 unlock()。

    2、Redisson 执行 Lua 脚本删除 Redis 中的锁 Key。

    3、同时主动取消看门狗定时任务,停止续期。

  • 阶段④ 异常兜底(客户端进程崩溃)

    1、若持有锁的客户端进程突然宕机,JVM 内的看门狗线程也随之消亡。

    2、Redis 中的锁 Key 将不再被续期,最多 30 秒后因 TTL 到期而自动释放,避免死锁。

9、Redlock 算法

Redlock(红锁)是 Redis 作者 Salvatore Sanfilippo(antirez) 提出的分布式锁算法。它的核心目标是解决一个致命问题:Redis 主从架构下,锁可能会丢失。

Redlock 要解决什么问题?

groovy 复制代码
正常情况:
  客户端A → Master加锁成功 → 数据同步到Slave → 锁生效 ✅

异常情况:
  客户端A → Master加锁成功(数据还没同步到Slave)
                              ↓
                      Master突然宕机! 
                              ↓
              Sentinel 把 Slave 提升为新Master
                              ↓
                  新Master中没有这把锁 ❌
                              ↓
  客户端B → 新Master加锁成功 → 同一把锁被两个人持有!
  • 现象:锁失效,分布式互斥被打破,数据可能错乱。

  • 原因:Redis 的主从复制是异步的,加锁成功后,数据可能还没同步到从节点,主节点就挂了。这是 Redis AP(可用性 + 分区容错性) 架构的天生缺陷------它优先保证可用性,而不是一致性。

Redlock 的思路

既然单个 Redis 主从集群不可靠,那我就用多个互相独立的 Redis Master 节点,加锁时超过半数节点同意才算成功。这样即使个别节点挂了,锁依然有效。

需要部署 N 个独立的 Redis Master 节点(通常 N = 5,且没有主从关系,全是独立的 Master)。

java 复制代码
步骤1:获取当前时间戳(毫秒),记为 T1

步骤2:依次向所有 5 个 Redis 节点发送加锁请求
       - 使用相同的 Key 和 Value(唯一值)
       - 设置一个**很短的超时时间**(如 5ms ~ 10ms)
       - 如果某个节点加锁失败或超时,立即尝试下一个节点

步骤3:计算加锁耗时
       加锁耗时 = 当前时间戳 - T1

步骤4:判断加锁是否成功
       成功条件:
       ① 超过半数节点加锁成功(N/2 + 1,即 5 个节点中至少 3 个)
       ② 加锁总耗时 < 锁的有效时间

步骤5:成功则持有锁执行业务;失败则向所有节点发送解锁请求

为什么加锁要设置很短的超时时间?

因为要快速失败。如果一个节点网络延迟很高或宕机,不能等它太久,否则会拖慢整个加锁过程。超时后立即跳过,去尝试下一个节点。

Redlock 的解锁流程

解锁时,无论加锁是否成功,都要向所有节点发送解锁请求:

因为要确保所有节点上的锁都被清理干净,避免残留锁导致后续加锁失败。

c 复制代码
客户端 → 所有 5 个 Redis Master 发送 DEL 命令
        ├── 节点1: DEL myLock  → 成功
        ├── 节点2: DEL myLock  → 成功
        ├── 节点3: DEL myLock  → 成功
        ├── 节点4: DEL myLock  → 成功(即使之前没加锁成功也发)
        └── 节点5: DEL myLock  → 成功

Redlock 的争议

争议一:依赖物理时钟

  • Redlock 在判断锁是否有效时,依赖客户端本地时间和Redis节点的物理时钟:
  • 加锁时记录 T1
  • 续期或解锁时比较当前时间与 T1
  • 如果系统时钟发生跳跃(比如 NTP 同步导致时间回拨),锁可能被误判为过期

争议二:网络延迟导致锁过期

c 复制代码
客户端A加锁成功,开始执行业务
业务执行时间超过了锁的有效期(因为网络延迟或GC暂停)
锁自动过期,客户端B拿到了锁
客户端A恢复后继续执行业务 → 两个客户端同时写数据 → 冲突

生产实践:到底用不用 Redlock?

绝大多数公司:不用 ❌

理由:

  • 部署成本高:需要 5 个独立的 Redis Master 节点,资源消耗大
  • 性能差:每次加锁要发 5 次网络请求,延迟高
  • 维护复杂:5 个节点的监控、告警、故障处理都比单节点复杂
  • 锁丢失风险可控:大多数业务场景(如商品详情页缓存、订单防重),锁丢失的后果并不严重,配合幂等处理即可

极少数场景:可以考虑 ✅

  • 金融核心交易(资金扣减、转账)
  • 需要严格遵守互斥的分布式调度
  • 公司有专门的 Redis 运维团队,且业务对一致性要求极高

总结:

Redlock 是在多个独立的 Redis Master 节点上同时加锁,只有超过半数节点加锁成功才算成功,以此避免单个 Redis 主从切换时锁丢失的问题。但它在业界争议极大,大多数生产场景不会使用。

10、跳跃表

跳跃表(Skip List)是一种有序数据结构,它通过在链表上建立多层索引,实现了类似二分查找的快速访问,同时保持了链表易于插入和删除的优点。

可以把它理解为:一个带有多级"快速通道"的有序链表。

跳跃表的结构图

c 复制代码
Level 3 (顶层索引):  1 ──────────────────────> 9 ──────────────────────> NIL
                     │                        │
Level 2 (中层索引):  1 ───────────> 5 ─────────> 9 ───────────> 13 ───> NIL
                     │            │            │            │
Level 1 (下层索引):  1 ───> 3 ───> 5 ───> 7 ───> 9 ───> 11 ──> 13 ──> NIL
                     │      │      │      │      │      │      │
Level 0 (原始数据):  1 ───> 2 ───> 3 ───> 4 ───> 5 ───> 6 ───> 7 ───> 8 ───> 9 ───> 10 ──> 11 ──> 12 ──> 13 ──> 14 ──> 15 ──> NIL

从顶层索引开始,快速定位到目标范围,再逐层向下精细查找,最后在底层链表中找到目标。这就是跳跃表的思路

11、MySQL 与Redis 如何保证双写一致性

方案一:旁路缓存模式(Cache-Aside)先更新数据库,再删除缓存

  • 执行流程:更新MySQL中的数据 → 删除Redis中对应的缓存
  • 优点:实现简单,脏数据存活时间极短(仅存在于更新数据库和删除缓存之间的毫秒级窗口),是业界最推荐的方案。
  • 缺点:如果删除缓存失败,会导致缓存中长期存在脏数据,需要配合重试机制。
  • 适用场景:绝大多数业务场景。
  • 补充机制:引入消息队列进行异步重试。当删除缓存失败时,将删除任务发送到消息队列,由消费者独立进行重试直至成功,保证删除操作最终一定被执行。

方案二:先删除缓存,再更新数据库(不推荐)

  • 执行流程:删除Redis缓存 → 更新MySQL中的数据
  • 优点:实现简单。
  • 缺点:并发场景下容易导致脏缓存被写入且长期存在。例如:线程A删除缓存后尚未更新数据库,线程B读请求发现缓存缺失,读取数据库旧值并回填缓存,随后线程A完成数据库更新,但缓存中已是旧数据,造成长时间不一致。
  • 结论:此方案存在严重缺陷,不推荐使用。

方案三:延迟双删

  • 执行流程:删除Redis缓存 → 更新MySQL中的数据 → 延迟一段时间 → 再次删除Redis缓存

  • 优点:第二次删除能将方案二中可能被其他线程写入的脏缓存清理掉,有效解决并发读写导致的数据不一致问题,实现成本低。

  • 缺点:延迟时间难以精确控制,设置过短可能清理不干净,设置过长会延长不一致窗口。

  • 适用场景:读并发较高的业务场景。

方案四:订阅MySQL Binlog异步删除

  • 执行流程:

    1、业务代码只负责更新MySQL,完全不操作Redis。

    2、引入一个中间件(如阿里的Canal),让它伪装成MySQL的从节点,实时监听MySQL的binlog(二进制日志)。

    3、一旦MySQL有数据变化,Canal就会收到消息,然后通过消息队列(MQ)将更新通知发给消费者。

    4、消费者收到消息后,负责更新或删除Redis中的对应缓存。

  • 优点:将缓存删除操作与业务代码完全解耦,业务系统无需关心缓存维护逻辑;利用Binlog的顺序性保证从根本上避免并发读写导致的不一致问题。

  • 缺点:引入Canal和消息队列增加了系统复杂度和运维成本;存在轻微延迟。

  • 适用场景:对一致性要求极高的核心系统,或希望将缓存维护下沉到中间层的场景。

方案五、分布式锁(互斥锁)

互斥锁的核心思想是"串行化",让并发写操作排队。标准的执行步骤如下:

  • 执行流程:
    1、尝试加锁(写锁):线程在执行业务前,先去Redis申请一把写锁(排他锁)。如果锁已被其他线程占用,则等待或超时失败。
    2、执行双写:成功拿到锁的线程,开始执行 update MySQL -> update/delete Redis 的操作。
    3、释放锁:双写操作完成后,主动释放锁,通知等待队列中的下一个线程继续执行。
    4、锁续期(Watchdog):如果业务执行时间较长,锁的守护线程会自动续期,防止锁在业务完成前过期释放(避免A线程没干完,B线程拿到锁导致脏数据)。
  • 优点:
    1、强一致性:在锁有效期内,读写操作完全串行,数据库与缓存的状态绝对一致,不存在中间态脏数据。
    2、逻辑简单:代码层面对开发者很直观,不容易出现Cache-Aside模式下因并发导致的缓存回溯问题。
    3、避免缓存雪崩:因为是同步更新/删除,不会出现"先删缓存、DB还没更新完时,大量请求打到DB"的情况(旁路模式的风险点)。
  • 缺点:
    1、性能断崖式下跌(致命伤):写操作吞吐量极低:所有写操作变成单线程排队,在高并发(如秒杀)场景下,系统QPS会骤降。
    2、RT(响应时间)增加:获取锁需要网络IO,加上锁等待时间,接口延迟显著增加。
    3、死锁风险:如果业务逻辑异常、服务宕机或网络抖动导致锁未释放,所有请求会永久阻塞。虽然Redisson有Watchdog自动续期和强制解锁机制,但依然需要复杂的兜底监控。
    3、主从架构下的锁失效:如果Redis部署了主从集群,当主节点加锁成功但数据还未同步到从节点时,主节点宕机,从节点升级为主节点,会导致锁丢失,从而让互斥失效(Redis本身的CAP缺陷)。
    4、复杂度过高:需要引入Redisson或自定义Lua脚本,增加了运维和调优成本。
  • 适用场景:由于性能损耗太大,互斥锁方案不是通用方案,只有在"写并发极高"且"绝对不允许脏数据"时才考虑

方案六:设置缓存过期时间作为兜底

  • 说明:此方案不独立解决双写一致性问题,而是作为以上所有方案的兜底机制。

  • 执行流程:为Redis缓存数据设置合理的过期时间(如30分钟或1小时)

  • 作用:即便上述方案因删除失败、并发脏写等原因导致缓存数据与数据库不一致,缓存过期后也会自动失效,后续读请求会从数据库重新加载最新数据,最终达成一致。

  • 适用场景:所有方案均需配合使用,是不可或缺的保障手段。

11、Redis 事务机制

Redis 事务提供了一种将多个命令打包、然后一次性按顺序执行的机制。与关系型数据库的事务不同,Redis 事务不支持回滚,其核心特性是原子性(命令队列中的命令要么全部执行,要么在入队阶段全部放弃)和隔离性(事务执行期间不会被其他客户端的命令打断)。Redis 通过 MULTI、EXEC、DISCARD 和 WATCH 四个命令来实现事务功能。

事务的生命周期

  • 第一阶段:事务开启

    客户端发送 MULTI 命令,Redis 为该客户端开启一个事务上下文,后续发送的命令不会立即执行,而是被放入一个命令队列中等待。

  • 第二阶段:命令入队

    在 MULTI 执行之后、EXEC 执行之前,客户端发送的所有命令(除 EXEC、DISCARD、WATCH 外)都会被 Redis 存入该客户端的事务队列中。此时每条命令执行后会返回 QUEUED 表示入队成功,而非命令的实际执行结果。

  • 第三阶段:事务执行

    客户端发送 EXEC 命令,Redis 将事务队列中的所有命令按入队顺序依次执行,执行过程中不会被其他客户端的命令打断(单线程特性天然保证原子性),执行完毕后将每个命令的结果按顺序打包返回给客户端。

  • 第四阶段:事务放弃

    客户端发送 DISCARD 命令,Redis 清空当前客户端的事务队列并退出事务上下文,所有已入队的命令均被丢弃,事务被取消。

WATCH 乐观锁机制

  • WATCH 命令用于实现乐观锁,它会在事务执行前对指定的 Key 进行监控。如果在调用 WATCH 之后、执行 EXEC 之前,被监控的 Key 被其他客户端修改了,那么该事务在执行时会被放弃,EXEC 返回 (nil),事务中的所有命令均不执行。WATCH 可以多次调用监控多个 Key,监控会在 EXEC 执行后自动取消,也可以通过 UNWATCH 命令手动取消所有监控。

  • 典型场景:实现 CAS(Compare-And-Swap)操作。客户端先 WATCH 某个 Key,读取其值,基于该值计算新值,然后通过 MULTI 开启事务并执行更新操作,最后 EXEC 提交。如果在读取值和提交事务期间该 Key 被其他客户端修改,则事务自动放弃,客户端可以重试整个流程。

  • 第一,WATCH不是锁。它不会阻塞别人,别人爱怎么改怎么改,只是你发现被改了就不执行了。这是乐观锁的思路。

  • 第二,WATCH只在EXEC时才触发检查。中间你可以做任何事,直到你发EXEC的那一刻,Redis才去检查监控的Key有没有被改过。

  • 第三,谁改了都算。不管是被其他客户端改的,还是被其他事务改的,只要WATCH之后有修改发生,你的EXEC就失败。

事务的关键特性与限制

  • 原子性(受限):Redis 事务具备命令队列层面的原子性,即所有命令会被一次性顺序执行,不会被其他客户端插入。但不支持回滚,如果事务中某条命令执行失败(如对 String 类型执行 LPUSH),其他命令仍然会继续执行,已经执行成功的命令不会撤销。只有在命令入队阶段发生语法错误时,整个事务才会在 EXEC 时被拒绝执行。

  • 隔离性:由于 Redis 是单线程模型,事务在执行期间完全不会被其他客户端的命令打断,天然具备最高级别的隔离性(串行化)。

  • 持久性:事务本身不提供持久性保证,持久化取决于是否开启了 AOF/RDB。

  • 不支持嵌套事务:在 MULTI 嵌套调用时,Redis 会直接返回错误。

12、Hash 冲突及解决方案

1、什么是 Hash 冲突

Hash 冲突(也称 Hash 碰撞)是指当两个或多个不同的 Key 经过 Hash 函数计算后,得到了相同的 Hash 值,从而被映射到同一个 Hash 槽位(数组索引位置)的现象。由于 Hash 表的数组长度是有限的,而输入 Key 的数量是无限的,根据鸽巢原理,Hash 冲突是不可避免的。

2、Redis 的 Hash 冲突解决方案

Redis 的字典(dict)结构采用链地址法(Separate Chaining)来解决 Hash 冲突,同时也配合渐进式 rehash 来从根本上减少冲突概率。

  • 方案一:链地址法(核心方案)

    Redis 的 Hash 表底层是一个数组,数组的每个元素是一个指向链表头部的指针。当发生 Hash 冲突时,Redis 将新元素以头插法插入到该槽位对应的链表头部。查找时,先通过 Hash 函数定位到数组槽位,然后遍历该槽位上的链表,逐个比较 Key 是否相等,直到找到目标元素或遍历结束。链地址法实现简单,能够有效处理冲突,但当链表过长时,查找性能会退化,Redis 通过配合 rehash 机制来控制链表长度。

  • 方案二:渐进式 rehash(辅助方案)

    当 Hash 表中的元素数量超过负载因子阈值时,Redis 会触发 rehash 操作,将 Hash 表扩容为原来的两倍。扩容后,原有的 Key 会重新计算 Hash 值并分散到更大的数组空间中,从而减少冲突概率。rehash 并非一次性完成,而是采用渐进式策略,将大量 rehash 工作分摊到后续每次对字典的增删改查操作中,每次操作时顺带迁移一小部分 Key,避免因一次性 rehash 导致主进程长时间阻塞。

  • 方案三:Hash 函数的选择

    Redis 使用 MurmurHash2 和 SipHash 等高质量 Hash 函数,能够将 Key 尽可能均匀地分布到各个槽位,从源头降低冲突概率。SipHash 还能有效抵御 Hash 碰撞攻击(如 HashDos 攻击)。

总结

Redis 通过链地址法作为 Hash 冲突的直接解决方案,将冲突的元素以链表形式串联在同一槽位;同时通过渐进式 rehash 动态扩容来分散数据,从根本上降低冲突概率;加上高质量的 Hash 函数保证数据均匀分布,三者结合确保 Redis 字典结构在高并发、大数据量场景下依然保持 O(1) 的平均查找性能。当链表长度过长时,查找性能会退化,但 rehash 机制能够及时扩容控制链表长度,维持整体性能稳定。

13、Redis 底层,使用什么协议

Redis 底层通信使用的是 RESP(Redis Serialization Protocol,Redis 序列化协议)。

这是一个专为 Redis 设计的、简单高效的文本协议,规定了客户端与服务器之间数据交换的格式

RESP 协议在设计上兼顾了以下三个目标,使其非常实用:

  • 实现简单 (Simple to implement):协议规则清晰,没有复杂的解析逻辑,方便开发者编写客户端库。

  • 解析快速 (Fast to parse):数据结构通过第一个字节即可识别,且使用 \r\n (CRLF) 作为分隔符,解析效率高。

  • 可读性强 (Human readable):协议本身是基于文本的,通过 telnet 或 nc 等工具就能直接发送命令进行调试,非常直观

相关推荐
susplus1 小时前
【linux应用软件编程】数据库sqlite3
linux·数据库·sqlite
SelectDB1 小时前
Apache Doris + Lance:让多模态数据真正进入智能驾驶与具身智能的分析闭环
数据库
疯狂打码的少年2 小时前
【数据库技术】复习日:关系代数 + SQL + 规范化(整理对比表)
jvm·数据库·笔记·sql
先吃饱再说2 小时前
从零开发到工程查询:SQLite 嵌入式数据库与高级 SQL 实战
数据库
hweiyu002 小时前
Redis命令:TTL
redis·缓存
ShineWinsu2 小时前
对于MySQL:数据库的操作的解析
linux·数据库·c++·mysql·面试·笔试·库的操作
是隼人2 小时前
buuctf-pwn mrctf2020_easy_equation(64位fmt)题解(学习过程持续更新)
c语言·学习·安全·pwn入门·ctf入门
‎ദ്ദിᵔ.˛.ᵔ₎2 小时前
MySQL 数据类型
数据库·mysql
这个DBA有点耶3 小时前
索引合并不是万能药:MySQL同时用两个索引,为什么比不用还慢?
数据库·mysql·dba