Redis 进阶实战:渐进式遍历、持久化机制与事务详解

  • [1. 渐进式遍历进式遍历](#1. 渐进式遍历进式遍历)
  • [2. Redis 数据库管理命令](#2. Redis 数据库管理命令)
  • [3. 持久化](#3. 持久化)
    • [3.1 RDB](#3.1 RDB)
      • [3.1.1 RDB 触发机制](#3.1.1 RDB 触发机制)
      • [3.1.2 RDB 文件的处理](#3.1.2 RDB 文件的处理)
      • [3.1.3 RDB 的特点](#3.1.3 RDB 的特点)
    • [3.2 AOF](#3.2 AOF)
      • [3.2.1 AOF 工作流程](#3.2.1 AOF 工作流程)
      • [3.2.2 命令写入](#3.2.2 命令写入)
      • [3.2.3 AOF 文件同步策略](#3.2.3 AOF 文件同步策略)
      • [3.2.4 AOF 文件的重写机制](#3.2.4 AOF 文件的重写机制)
      • [3.2.5 启动时数据恢复](#3.2.5 启动时数据恢复)
  • [4. Redis 事务](#4. Redis 事务)
    • [4.1 什么是事务](#4.1 什么是事务)
    • [4.2 事务操作](#4.2 事务操作)
  • [5. 小结](#5. 小结)
  • [6. 经典面试题](#6. 经典面试题)

✅️ 1.渐进式遍历

Redis使用scan命令进行渐进式遍历键,进而解决直接使用keys获取键值时可能出现的阻塞问题。每次执行scan命令的时间复杂度是O(1),但是要完整的遍历完成所有键的遍历,需要执行多次scan.

📌 注意

首次执行scan命令从0开始。

当scan返回的下次位置为0 时,遍历结束。

渐进式遍历命令其实就是一组命令 ,而不是一条命令。

语法 :SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

📚 理解:

  1. cursor光标不能简单的理解成为"下标",不是一个连续递增的整数,仅仅是一个字符串,光标这个概念,程序员、客户端是无法理解的,只有Redis服务器内部知道这个光标对应的位置。
  2. pattern 表示匹配模式 count代表当前扫描的个数
  3. 此处的count只是给Redis服务器一个提示或者建议,写入的count和实际返回的key的个数不一定完全是相同的,但是不会相差很多。
  4. 光标设置为0,表示当前遍历是从头开始的
  5. 光标的返回值表示,下次遍历时,光标从哪里开始。
  6. 这里的渐进式遍历,在遍历的过程中,不会在服务器这边存储任何的状态信息,此处的的遍历随时可以终止 -- 不会对服务器产生任何的副作用。
  7. 尽管使用scan命令获取key比直接使用keys * 要好点,但是使用的时候还是有很多的注意事项,有很多出错的可能性。
  8. 渐进性遍历 scan 虽然解决了阻塞的问题,但如果在遍历期间键有所变化(增加、修改、删除),可能导致遍历时键的重复遍历或者遗漏,这点务必在实际开发中考虑。

示例

bash 复制代码
redis 127.0.0.1:6379> scan 0
1) "17"
2) 1) "key:12"
 2) "key:8"
 3) "key:4"
 4) "key:14"
 5) "key:16"
 6) "key:17"
 7) "key:15"
 8) "key:10"
 9) "key:3"
 10) "key:7"
 11) "key:1"
redis 127.0.0.1:6379> scan 17
1) "0"
2) 1) "key:5"
 2) "key:18"
 3) "key:0"
 4) "key:2"
 5) "key:19"
 6) "key:13"
 7) "key:6"
 8) "key:9"
 9) "key:11"

2. Redis 数据库管理命令

Redis提供了几个针对于Redis数据库的命令:dbsize select flushdb flushall.

3. 持久化

❓问题引入

Redis是一个内存级数据库,将数据放到内存中,如果进程异常退出或者断电,会导致数据的丢失,所以想要持久化,就需要将内存中的数据存放的奥磁盘中进行备份。对于Redis,使用RBD(Redis DataBase)和AOF(Append Only File)两种持久化机制来保证数据的持久性。

3.1 RDB

RDB持久化是把当前进程数据生成快照保存到硬盘中的过程 ,触发RDB持久化过程分为手动触发 和自动触发。

3.1.1 RDB触发机制

① 手动触发

  • save命令:执行当前命令时会阻塞当前Redis服务器主进程,知道RDB持久化过程完成,对于内存使用量比较大的示例会造成长时间阻塞,所以这个命令基本不使用。
  • bgsave命令:Redis进程执行fork操作 创建子进程,RDB持久化任务由这个子进程来完成,完成后自动结束。阻塞一般发生在fork阶段,时间很短,影响很小。所以Redis内部的所有涉及RDB的操作都采用类似bgsave的方式。

下图是使用bgsave命令完成RDB持久化原理图:

如果执行的是save命令,此时是不会触发子进程、文件替换逻辑的。

如果是save就直接在主进程中,网刚才的同一个文件中写入数据了。

② 自动触发 - 实战中最常用的几种方式

  • 使用save配置,如"save m n "表示m秒内数据集发生了n次修改,自动进行RDB持久化。
  • 从节点进行全量复制时,主节点自动进行RDB持久化,然后将RDB文件内容发送给从节点。
  • 执行shutdown命令关闭Redis时,执行RDB持久化。
3.1.2 RDB文件的处理

保存 :RDB ⽂件保存再 dir 配置指定的⽬录(默认 /var/lib/redis/)下,⽂件名通过 dbfilename配置(默认 dump.rdb)指定。可以通过执⾏config set dir {newDir}和 config set dbfilename {newFilename} 运⾏期间动态执⾏,当下次运⾏时RDB⽂件会保存到新⽬录。

压缩 :Redis 默认采⽤LZF算法对⽣成的 RDB ⽂件做压缩处理,压缩后的⽂件远远⼩于内存⼤⼩,默认开启,可以通过参数 config set rdbcompression {yes|no} 动态修改。

校验 :如果Redis启动时加载到损坏的 RDB ⽂件会拒绝启动。这时可以使⽤ Redis 提供的redis-check-dump ⼯具检测 RDB ⽂件并获取对应的错误报告。

💡 1. 虽然压缩 RDB 会消耗 CPU资源,但可以⼤幅降低⽂件的体积,⽅便保存到硬盘或通过⽹络发送到从节点,因此建议开启。

  1. RDB持久化操作是可以执行多次的

  2. 当执行生成rdb镜像操作的时候,此时就会把要生成的快照数据,先保存到一个临时文件中,当这个快照数据生成完毕以后,再删除之前的rdb文件,把新生成的临时的rdb文件名字改成刚才的dump.rdb.

  3. 对于redis来说,修改配置文件后,需要重启一下服务器才能生效。

3.1.3 RDB的特点
  • RDB 是⼀个紧凑压缩的⼆进制⽂件,代表 Redis 在某个时间点上的数据快照。⾮常适⽤于备份,全量复制等场景。⽐如每 6 ⼩时执⾏ bgsave 备份,并把 RDB⽂件复制到远程机器或者⽂件系统中(如 hdfs)⽤于灾备。
  • Redis 加载 RDB 恢复数据远远快于 AOF 的⽅式。
  • RDB ⽅式数据没办法做到实时持久化 / 秒级持久化。因为 bgsave 每次运⾏都要执⾏fork创建⼦进程,属于重量级操作,频繁执⾏成本过⾼。
  • RDB ⽂件使⽤特定⼆进制格式保存,Redis 版本演进过程中有多个 RDB 版本,兼容性可能有⻛险。
3.2 AOF
3.2.1 AOF 工作流程

AOF(Append Only File)持久化:以独⽴⽇志的⽅式记录每次写命令,重启时再重新执⾏ AOF⽂件中的命令达到恢复数据的⽬的。AOF 的主要作⽤是解决了数据持久化的实时性,⽬前已经是Redis 持久化的主流⽅式。

开启 AOF 功能需要设置配置:appendonly yes,默认不开启。AOF ⽂件名通过appendfilename 配置(默认是 appendonly.aof)设置。保存⽬录同 RDB 持久化⽅式⼀致,通过 dir配置指定。AOF 的⼯作流程操作:命令写⼊(append)、⽂件同步(sync)、⽂件重写(rewrite)、重启加载(load)。

下图是AOF的工作流程图:

3.2.2 命令写入

AOF 命令写⼊的内容直接是⽂本协议格式。例如 set hello world 这条命令,在 AOF 缓冲区会追加如下⽂本:

*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n

此处遵守 Redis 格式协议,Redis 选择⽂本协议可能的原因:⽂本协议具备较好的兼容性;实现简单;具备可读性。

AOF 过程中为什么需要 aof_buf 这个缓冲区? Redis 使⽤单线程响应命令,如果每次写 AOF ⽂件都直接同步硬盘,性能从内存的读写变成 IO 读写,必然会下降。先写⼊缓冲区可以有效减少 IO 次数,同时,Redis 还可以提供多种缓冲区同步策略,让⽤⼾根据⾃⼰的需求做出合理的平衡。

3.2.3 AOF文件同步策略
3.2.4 AOF文件的重写机制

❓️ 拓展问题1 :子进程里面的内存数据是父进程fork之前的状态,fork之后,新来的请求,对内存造成的修改,是子进程不知道的。

解决方案 :子进程把aof文件数据写完之后,会通过信号通知一下父进程,父进程再将aof_rewrite_buf缓冲区中的内容写到新AOF文件中,就可以用新的AOF文件代替旧的AOF文件。

拓展问题2 :父进程fork完毕之后,就已经让子进程写新的aof文件了,而且随着时间的推移,子进程很快就写完了新的文件,要让新的aof文件替代旧的~~ 父进程此时还在继续写这个将亡的旧的aof文件是否还有意义?{不能不写}

原因 :考虑到极端情况,假设再重写过程中,服务器挂了,子进程内存的数据就会丢失,新的aof文件内容还不完整,所以如果父进程不坚持写旧aof文件,重启旧没法保证数据的完整性了。
🐳 理解:

  1. 重写的时候,并不关心aof文件中原来都有什么内容,只是关心内存中最终的数据状态即可。子进程只需要把当前内存中的数据读取出来,以AOF的文件格式写入到一个全新的AOF文件中。
  2. 内存中的数据的状态,就已经相当于是把AOF文件结果整理后的模样了。
    📌 结论:因此Redis就存在一种机制,能够针对文件进行整理操作,这个整理就是能够剔除其中的冗余操作,并且合并一些操作,达到给AOF文件 瘦身 的效果。
3.2.5 启动时数据恢复

当 Redis 启动时,会根据RDB和 AOF ⽂件的内容,进⾏数据恢复。

混合持久化:结合rdb和aof的特点~~

AOF本来是按照文本的方式来写入文件的,但是文本的方式写文件,后续加载的成本是非常高的.

此时Redis就引入了"混合持久化"的方式-- 结合了rdb和aof的特点

按照aof的方式,每一个请求和操作都记录入文件,在触发重写之后,就会把当前内存的状态按照按照rdb的二进制格式写入到新的aof文件中,后续再进行的操作,仍然是按照aof文本的方式追加到文件

4. Redis 事务

4.1 什么是事务
4.2 事务操作

MULTI 开启一个事务,执行成功后返回OK,EXEC表示执行事务,两者配合使用。

示例:

bash 复制代码
127.0.0.1:6379> MULTI # 开启一个事务
OK
127.0.0.1:6379> set key 100
QUEUED
127.0.0.1:6379> set key 200
QUEUED
127.0.0.1:6379> set key 300
QUEUED
127.0.0.1:6379> EXEC  # 执行一个事务
1) OK
2) OK
3) OK
127.0.0.1:6379> get key
"300"

📌 说明:每次在事务中添加一个操作,都会提示一个"QUEUED",说明命令已经进入到客户端的事务队列中,当执行EXEC命令时,客户端才会将事务队列中的命令发送到服务器中。

DISCARD 放弃当前事务,此时会直接清空事务队列,之前的操作都不会被执行。

示例:

bash 复制代码
127.0.0.1:6379> multi  # 开启一个事务
OK
127.0.0.1:6379> set fruit apple
QUEUED
127.0.0.1:6379> set fruit pear
QUEUED
127.0.0.1:6379> discard # 放弃一个事务
OK
127.0.0.1:6379> get fruit  # 由于事务队列中的命令没有执行,导致fruit中为空
(nil)

在执行事务的过程中,如果某个事务中正在修改的值被其他的客户端修改了,此时就容易出现数据不一致的问题。watch 命令就是⽤来解决这个问题的. watch 在该客⼾端上监控⼀组具体的 key.

  • 当开启事务的时候, 如果对 watch 的 key 进⾏修改, 就会记录当前key的 "版本号". (版本号是个简单的整数, 每次修改都会使版本变⼤. 服务器来维护每个 key 的版本号情况)
  • 在真正提交事务的时候, 如果发现当前服务器上的 key 的版本号已经超过了事务开始时的版本号, 就会让事务执⾏失败. (事务中的所有操作都不执⾏).

示例:

bash 复制代码
## 客户端1执行过程
127.0.0.1:6379> watch k1 # 开始监控 k1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 100 # 进⾏修改, 从服务器获取 k1 的版本号是 0. 记录 k1 的版本号. (还没真修改呢, 版本号不变)
QUEUED
127.0.0.1:6379> set k2 1000 
QUEUED

## 客户端2执行过程
127.0.0.1:6379> set k1 200 # 修改成功, 使服务器端的 k1 的版本号 0 -> 1
OK

## 客户端执行过程
127.0.0.1:6379> EXEC # 真正执⾏修改操作, 此时对⽐版本发现, 客⼾端的 k1 的版本号是 0, 服务器上的版本号是 1, 版本不⼀致! 说明有其他客⼾端在事务中间修改了 k1 !!! 
(nil)
127.0.0.1:6379> get k1
"200"
127.0.0.1:6379> get k2
(nil)
# 此时说明事务已经被取消了. 这次提交的所有命令都没有执⾏.
# unwatch相当于watch命令的逆操作

🏠 扩展知识

watch的实现就类似于一个"乐观锁"。

乐观锁和悲观锁不是指某个具体的锁,而是指的是某一类锁的特性

乐观锁:加锁之前,就有一个心理预期,预期接下来的冲突的概率比较低。

悲观锁:加锁之前,就有一个心理预期,接下来锁冲突的概率比较大。

watch的实现就相当于基于版本号来实现的"乐观锁"。

✅️ 5.小结

本文围绕 Redis 的高阶用法与可靠性保障,梳理了四个核心知识点:

  • 渐进式遍历 :scan 命令以 O(1) 的单次复杂度分批遍历键,避免了 keys * 造成的阻塞;但遍历期间键的增删改可能导致重复或遗漏,且 count 只是建议值,cursor 为 0 表示遍历结束。
  • 数据库管理命令 :dbsize、select、flushdb、flushall 用于查看键数量、切换库以及清空数据,其中 flushall 会清空所有库,需谨慎使用。
  • 持久化机制 :
    • RDB 以二进制快照形式保存某一时刻的全量数据,文件紧凑、恢复快,适合备份与全量复制,但无法做到实时持久化;
    • AOF 以文本日志方式记录每次写命令,实时性更好,通过 aof_buf 缓冲与同步策略在性能与安全间平衡,并借助重写机制剔除冗余命令实现"瘦身";
    • 混合持久化结合两者优势,重写时以 RDB 二进制格式写入当前内存状态,后续操作继续以 AOF 文本追加。
  • Redis 事务 :MULTI 开启、EXEC 提交、DISCARD 放弃,命令先入客户端事务队列;WATCH 基于版本号实现乐观锁,在提交时校验键是否被其他客户端修改,从而保证数据一致性。

一句话总结:用 scan 替代 keys 保证遍历安全,用 RDB + AOF 保证数据不丢,用 MULTI/EXEC/WATCH 保证操作原子与一致。

✅️ 6.经典面试题

Q1:keys * 和 scan 有什么区别?为什么生产环境推荐用 scan?

keys * 会一次性遍历所有键,时间复杂度为 O(N),在键数量庞大时会长时间阻塞 Redis 单线程;scan 采用渐进式遍历,每次执行复杂度为 O(1),通过游标分批返回结果,不会阻塞服务器。但 scan 在遍历期间若键发生变化,可能出现重复或遗漏,且 count 只是提示值,实际返回数量不固定。

Q2:RDB 的 save 和 bgsave 有什么区别?

save 在主进程中同步执行,会阻塞整个 Redis 服务器直到持久化完成,内存数据量大时阻塞时间很长,基本不推荐使用;bgsave 通过 fork 创建子进程异步完成持久化,仅在 fork 阶段短暂阻塞主进程,影响很小,是 Redis 内部 RDB 操作的主要方式。

Q3:RDB 和 AOF 各自的优缺点是什么?如何选择?

RDB 是紧凑的二进制快照,文件体积小、恢复速度快,适合备份和全量复制;但无法做到秒级持久化,fork 成本高,且不同版本间存在兼容性风险。AOF 以文本记录写命令,实时性好、可读性强,但文件体积大、恢复速度慢。实际生产中通常两者结合,或采用混合持久化:以 RDB 格式保存基线数据,以 AOF 追加增量操作。

Q4:AOF 为什么需要 aof_buf 缓冲区?同步策略有哪些?

Redis 是单线程响应命令,若每次写 AOF 都直接同步磁盘,性能会从内存读写退化为 IO 读写。引入 aof_buf 可以先写缓冲区,减少 IO 次数,并提供多种同步策略(如 always、everysec、no),让用户在性能与数据安全之间做平衡。

Q5:AOF 重写时,父进程为什么还要继续写旧的 AOF 文件?

重写由子进程基于 fork 时刻的内存快照生成新 AOF 文件,期间新请求的修改子进程并不知道,父进程会将其写入 aof_rewrite_buf,待子进程完成后合并到新文件。父进程继续写旧 AOF 是为了兜底:若重写过程中服务器宕机,新文件尚不完整,旧文件仍能保证数据完整性。

Q6:Redis 事务和关系型数据库事务有什么区别?

Redis 事务通过 MULTI 将命令入队、EXEC 一次性提交执行,具备一定的原子性,但不支持回滚 :若某条命令运行时出错,其余命令仍会执行。它也不支持隔离级别,只能通过 WATCH 基于版本号实现乐观锁,在提交时检测键是否被修改,若被修改则整个事务放弃执行。

Q7:WATCH 的实现原理是什么?它属于乐观锁还是悲观锁?

WATCH 会记录被监控键的版本号,EXEC 提交时对比服务器上键的当前版本号,若不一致说明事务期间有其他客户端修改了该键,事务整体不执行。它属于乐观锁:假设冲突概率较低,不加锁而是提交时校验,冲突则重试,避免了悲观锁长时间持有锁带来的开销。

相关推荐
普通人61 小时前
MySQL_FIND_IN_SET函数详解
android·数据库·mysql
真上帝的左手1 小时前
4. 关系型数据库-MySQL-Canal 进阶
数据库·mysql·canal
xiaoye-duck1 小时前
《C++ 项目:手写大模型接入 SDK》DataManager数据管理模块:SQLite持久化存储
开发语言·数据库·c++·sqlite
唐老板2 小时前
uuidv7 来了:为什么别再拿 uuidv4 当主键了
数据库
SelectDB15 小时前
让变更流出 Doris:Apache Doris 5.0 基于增量计算的 CDC 技术解析
数据库
这个DBA有点耶15 小时前
MCP隔离层+行级语义审计+输出校验:AI访问数据库的安全边界拆解
数据库·人工智能·ai编程
这个DBA有点耶16 小时前
从反向扫描O(n)到正向扫描O(1):降序索引的物理机制与混合排序匹配规则
数据库