目录
1. Redis持久化机制(RDB与AOF)
1.1. RDB(Redis DataBase):内存快照
1.1.1 触发机制
1.1.2 bgsave 运作流程
1.1.3 优缺点
1.1.4 RDB文件的处理
1.2. AOF(Append Only File):日志追加
1.2.1 工作流程(append -> sync -> rewrite -> load)
1.2.2 文件同步策略(appendfsync)
1.2.3 AOF重写机制
1.2.4 启动时数据恢复
Redis 4.0 的混合持久化
2. Redis事务与乐观锁
2.1. Redis事务的ACID特性
2.2. 核心命令与执行流程
2.3. WATCH机制:乐观锁(CAS)的实现
2.3.1 WATCH 原理(版本号机制)
2.3.2 实操演示
2.3.3 UNWATCH
3. 高频面试题 Q&A 速查表
1. Redis持久化机制(RDB与AOF)
Redis之所以快,是因为数据都在内存中
但为了防止进程崩溃或宕机导致数据丢失,必须将数据落盘,这就是持久化
1.1. RDB(Redis DataBase):内存快照
RDB是将当前进程的数据生成快照保存到硬盘的过程,生成的是紧凑压缩的二进制文件
1.1.1 触发机制
手动触发:
- save:阻塞当前Redis服务器,直到RDB完成。基本废弃
- bgsave:Redis进程执行fork创建子进程,由子进程负责持久化,父进程阻塞仅发生在fork阶段。主流方式
自动触发:
- 配置 save m n(m秒内发生n次修改则触发)
- 主从全量复制时,主节点自动执行bgsave
- 执行 shutdown 命令关闭Redis时
1.1.2 bgsave 运作流程
- 父进程判断当前是否存在正在执行的子进程(如RDB/AOF子进程),存在则直接返回
- 父进程执行 fork 创建子进程(此阶段父进程阻塞,可通过 info stats 的 latest_fork_usec 查看耗时微秒数)
- 父进程 fork 完成后,返回 "Background saving started",不再阻塞,继续响应其他命令
- 子进程根据父进程内存快照(Copy-On-Write,写时复制机制)生成临时RDB文件,完成后对原有文件进行原子替换
- 子进程发送信号给父进程,父进程更新统计信息(rdb_last_save_time)
- fork时父进程在干什么?
父进程继续响应其他命令。利用Linux的写时复制机制,子进程共享父进程的内存页,只有当父进程修改数据时,才会复制一份新的内存页。这保证了极低的性能损耗
1.1.3 优缺点
- 优点:文件紧凑,适合备份和全量复制;恢复速度快(远快于AOF)
- 缺点:无法做到实时/秒级持久化(fork是重量级操作,频繁执行成本高);版本演进兼容性有风险(特定二进制格式)
1.1.4 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} 动态修改;压缩虽然会消耗一定 CPU,但能大幅减小文件体积,便于硬盘保存和主从节点网络传输,因此建议保持开启
- 校验:Redis 启动时如果加载到损坏的 RDB 文件会拒绝启动,此时可使用 redis-check-dump(新版本中为 redis-check-rdb)检测 RDB 文件并获取错误报告,生产环境应定期校验备份文件,发现损坏时优先从备份或从节点恢复
1.2. AOF(Append Only File):日志追加
AOF以独立日志的方式记录每次写命令,重启时重新执行AOF文件中的命令来恢复数据
目前是Redis持久化的主流方式
开启 AOF 功能需要设置配置:appendonly yes,默认不开启。AOF 文件名通过 appendfilename 配置(默认是 appendonly.aof)设置。保存目录同 RDB 持久化方式一致,通过 dir 配置指定
1.2.1 工作流程(append -> sync -> rewrite -> load)
- 命令写入:所有写命令追加到 aof_buf(缓冲区)中
- 文件同步:根据策略将缓冲区同步到硬盘
- 文件重写:定期压缩AOF文件
- 重启加载:启动时加载AOF文件恢复数据
- 为什么要有 aof_buf 缓冲区
Redis是单线程响应命令,如果每次都直接同步硬盘(IO操作),性能会急剧下降。先写缓冲区可以减少IO次数,同时允许提供多种同步策略来平衡性能与安全
1.2.2 文件同步策略(appendfsync)
| appendfsync 配置值 | 说明 | fsync 时机 | 建议 |
|---|---|---|---|
| always | 命令写入aof_buf后调用fsync同步,完成后返回 | 每次写入都执行fsync | 除非是非常重要的数据,否则不建议配置 |
| everysec | 命令写入aof_buf后只执行write,不立即fsync;每秒由同步线程执行一次fsync | 每秒同步一次 | 默认配置,也是推荐配置 |
| no | 命令写入aof_buf后只执行write,由操作系统控制fsync频率 | 由 OS 控制,不可控 | 除非数据重要程度很低,一般不建议配置 |
- always:每次写入都fsync。最安全,性能最差(SATA硬盘仅支持几百TPS)
- everysec:每秒由同步线程fsync一次。默认且推荐配置,兼顾性能与安全,理论上最多丢失1秒数据
- no:由操作系统控制fsync频率。性能最高,但数据丢失风险大
| 对比项 | write 操作 | fsync 操作 |
|---|---|---|
| 核心机制 | 延迟写(delayed write),利用 Linux 内核页缓冲区 | 强制硬盘同步 |
| 操作对象 | 文件描述符 / 文件 | 单个文件 |
| 返回时机 | 写入系统缓冲区后立即返回 | 阻塞直到数据写入硬盘 |
| 是否阻塞 | 通常不阻塞 | 阻塞 |
| 落盘保证 | 不保证立即落盘,依赖系统调度 | 强制同步,等待落盘完成 |
| 同步触发 | 缓冲区页空间写满、达到特定时间周期等 | 显式调用触发 |
| 故障风险 | 同步前系统故障宕机,缓冲区内数据可能丢失 | 调用返回后,正常情况下数据已写入硬盘 |
| 性能影响 | 性能高,减少磁盘 IO | 性能开销大,增加延迟 |
| 适用场景 | 常规写入,追求吞吐量 | 需要持久化保证的关键数据 |
1.2.3 AOF重写机制
随着命令不断写入,AOF文件会越来越大,Redis引入重写机制来压缩体积
- 为什么能变小: 超时数据不再写入;无效命令(如被覆盖的set)被删除;多条写操作合并(如多次lpush合并为一条)
- 触发时机:手动执行 bgrewriteaof,或根据 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 自动触发
| 配置项 | 说明 | 默认值 |
|---|---|---|
auto-aof-rewrite-min-size | 表示触发重写时 AOF 的最小文件大小 | 64MB |
auto-aof-rewrite-percentage | 表示当前 AOF 占用大小相比较上次重写时增加的百分比 | — |
AOF重写流程与并发细节
- 执行重写请求
如果当前进程正在执行 AOF 重写,请求不执行;
如果当前有bgsave在执行,则延迟到其完成后再执行 - 父进程 fork 创建子进程
- 关键并发处理:
- 父进程继续响应其他命令。所有修改操作写入 aof_buf,并根据策略同步到旧AOF文件,保证旧AOF机制正确
- 由于子进程只有 fork 之前的内存信息,父进程需要将 fork 之后的修改操作写入 aof_rewrite_buf(AOF重写缓冲区)
- 子进程根据内存快照,将命令合并写入新的AOF文件
- 子进程完成后发送信号给父进程
- 父进程将 aof_rewrite_buf 中的命令追加到新AOF文件,并用新AOF文件原子替换老AOF文件
1.2.4 启动时数据恢复
当 Redis 启动时,会根据 RDB 和 AOF 文件的内容,进行数据恢复
当Redis启动时,如果同时存在RDB和AOF文件,优先加载AOF(因为AOF数据更全)。如果AOF不存在,则加载RDB。如果加载损坏的文件,Redis会拒绝启动(可用 redis-check-aof 或 redis-check-dump 修复)
Redis 4.0 的混合持久化
通过 aof-use-rdb-preamble yes 开启。开启后,AOF 重写时子进程先将当前内存数据以 RDB 格式写入新 AOF 文件头部;重写期间及之后的新写命令以 AOF 格式追加到文件后面。重启加载时,先加载 RDB 头部,再重放后续 AOF 命令。这样既利用 RDB 加载快、体积小的优点,又保留 AOF 的增量记录,降低数据丢失风险。但数据丢失窗口仍取决于 appendfsync 策略,且混合 AOF 文件对旧版本 Redis 和部分工具兼容性较差
2. Redis事务与乐观锁
Redis的事务与MySQL的事务概念类似,都是将一系列操作绑定成一组批量执行,但在ACID特性上有极大的弱化
2.1. Redis事务的ACID特性
- 弱化的原子性:Redis没有“回滚机制”。只能做到“批量执行”,不能做到“一个失败就恢复到初始状态”。如果事务中某条命令执行失败(如类型错误),其余命令仍会继续执行
- 满足一致性:无约束,不存在违反约束的非法状态。中间状态非法,需要业务自己判断
- 天然隔离性:Redis单线程处理请求,天然串行执行,不存在并发执行事务的问题
- 不保证持久性:数据在内存中,是否持久化由RDB/AOF机制决定,与事务无关
2.2. 核心命令与执行流程
- MULTI:开启事务。返回OK
- 命令入队:后续的命令不会立即执行,而是返回 QUEUED(放入客户端/服务器的事务队列)
- EXEC:真正执行事务。按顺序执行队列中的命令
- DISCARD:放弃当前事务,清空事务队列
事务的错误处理
- 入队错误(语法错误、命令不存在):在Redis 2.6.5之后,如果入队阶段发生错误,EXEC 会直接返回错误,整个事务都会被丢弃
- 运行时错误(如对String执行lpush,或incr一个非数字):命令在入队时不会报错,EXEC后,正确的命令会执行,错误的命令报错,且不支持回滚。这是Redis事务与关系型数据库最大的区别
2.3. WATCH机制
在并发场景下,客户端可能先读取数据,再基于旧值构造事务进行修改。如果在提交事务前,该数据已被其他客户端修改,直接提交就可能造成数据不一致(如丢失更新)。Redis 通过 WATCH 命令实现乐观锁(CAS):WATCH 监视相关 key,若在 WATCH 之后、EXEC 之前这些 key 被修改过,则 EXEC 放弃执行整个事务并返回 nil,从而避免基于过期数据做出错误修改
2.3.1 WATCH 原理
- Redis 通过 WATCH 实现乐观锁。Redis 建立映射:key -> 监视该 key 的客户端列表,客户端也记录自己监视了哪些 key。在 WATCH 之后、EXEC 之前,只要任何客户端(包括自己)修改了被监视的 key,Redis 就会通过 touchWatchedKey 给监视该 key 的客户端打上 CLIENT_DIRTY_CAS 标志。执行 EXEC 时,如果客户端带有这个标志,就放弃整个事务并返回 nil,事务中的命令都不执行;否则正常执行。EXEC 后自动取消所有 WATCH
2.3.2 实操演示
# 客户端1 WATCH k1 # 记录 k1 的版本号(假设是 0) MULTI set k1 100 # 入队,但不提交 set k2 1000 # 入队 # 客户端2(此时执行) set k1 200 # 修改成功,k1 版本号变为 1 # 客户端1 再执行 EXEC # 比对版本号:客户端是 0,服务器是 1。版本不一致! (nil) # 事务失败,所有命令都不执行2.3.3 UNWATCH
取消对所有key的监控,相当于WATCH的逆操作
EXEC 和 DISCARD 执行后,也会自动取消所有WATCH
WATCH 适合什么场景
WATCH 本质是CAS(Compare And Swap)思路,在很多并发编程和数据库(如MySQL的乐观锁)中都有应用。它适合读多写少的并发场景,如果在高并发写场景下,事务很容易因为版本冲突而不断重试失败,影响性能
3. 高频面试题 Q&A 速查表
Q1:RDB和AOF的区别?如何选择
- RDB:紧凑二进制,恢复快,适合冷备和主从复制。但实时性差,fork开销大
- AOF:文本协议,实时性好(默认每秒同步),可读性强。但文件体积大,恢复慢
- 选择:如果对数据安全性要求极高,选AOF(everysec);如果追求恢复速度,容忍少量数据丢失,选RDB;生产环境推荐混合持久化(Redis 4.0+)
Q2:AOF重写时,主进程在做什么
- 主进程继续响应其他客户端请求
- 将新写入的命令追加到 aof_buf 以同步到旧AOF文件
- 将新写入的命令同时追加到 aof_rewrite_buf,等待子进程重写完成后,追加到新AOF文件,保证数据不丢失
Q3:Redis事务支持原子性吗
- 不支持严格的原子性。它只保证命令的“批量执行”和“连续执行不被加塞”
- 如果发生运行时错误(如类型错误),Redis不会回滚,其余命令会继续执行
Q4:WATCH命令的实现原理
- 基于版本号(CAS乐观锁)。WATCH记录key的当前版本号,EXEC时比对,如果版本号发生变化(被其他客户端修改),则拒绝执行事务,返回nil
Q5:为什么Redis执行bgsave时,不阻塞主进程
- 利用操作系统的 fork 和 写时复制(Copy-On-Write) 机制。fork出的子进程共享父进程内存,只有当父进程有写操作时,才会复制被修改的内存页,极大降低了内存开销和阻塞时间