Redis 所有的数据和状态存储在内存中,为了避免进程退出而导致数据丢失,需要将数据和状态保存到硬盘上。
为了达到这一目的,通常有两种实现方式:
- 将 Redis 当作一个状态机,记录每一次的对 Redis 的操作,也就是状态转移。需要恢复时再从初始状态开始,依次重放记录的操作,这样的方式称作逻辑备份
- 将 Redis 完整的状态保存下来,待必要时原样恢复,这样的方式称作物理备份
Redis 也实现了这两种持久化方式,分别时 AOF 和 RDB
AOF
AOF 通过保存 Redis 服务器执行的写命令记录数据库状态。
AOF 配置
Redis 源码中的配置文件示例: redis.conf
AOF 配置示例
https://github.com/redis/redi...
重要参数:
appendonly yes # 是否开启 AOF,如果开启了 AOF,后续恢复数据库时会优先使用 AOF,跳过 RDB
appendfsync everysec # 持久化判断规则
appendfilename appendonly.aof # AOF 文件位置void flushAppendOnlyFile(int force) {
ssize_t nwritten;
...
// 调用 write 写入文件
nwritten = write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));
...
// 成功写入后
server.aof_current_size += nwritten;
...
// 根据 appndfsync 条件判断是否同步到 AOF 文件
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
...
// 这里强制执行同步用的是 aof_fsync,是因为 aof_fsync 已经被定义成了 fsync
// 具体位置在 config.h:https://github.com/redis/redis/blob/48e24d54b736b162617112ce27ec724b9290592e/src/config.h#L89
aof_fsync(server.aof_fd);
...
// 成功后记录下时间,用于下一次同步条件检查
server.aof_last_fsync = server.unixtime;
} else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
server.unixtime > server.aof_last_fsync)) {
// 在另一个线程中后台执行
if (!sync_in_progress) aof_background_fsync(server.aof_fd);
server.aof_last_fsync = server.unixtime;
}
}# RDB 配置示例
# https://github.com/redis/redis/blob/48e24d54b736b162617112ce27ec724b9290592e/redis.conf#L125
# 重要参数:
dbfilename dump.rdb
dir /var/lib/redis// https://github.com/redis/redis/blob/48e24d54b736b162617112ce27ec724b9290592e/src/redis.c#L1199
// 开始检查自动保存条件前会先检查是否有正在后台执行的 RDB 和 AOF 进程
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
// 已有后台的 RDB 或 AOF 进程
} else {
// 遍历 saveparams 链表中所有的配置条件
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
/* 满足自动保存的标准:
1. 从上次完成 RDB 到现在的数据库修改次数(dirty)已经达到了 save 配置中 changes 的值
2. 距上一次完成 RDB 的时间(lastsave)已经达到了 save 配置中 seconds 的值
3. 上一次 RDB 已经成功,或者距上一次尝试 RDB 的时间(lastbgsave_try)已经达到了配置的超时时间(REDIS_BGSAVE_RETRY_DELAY)
*/
if (server.dirty >= sp->changes &&
server.unixtime-server.lastsave > sp->seconds &&
(server.unixtime-server.lastbgsave_try >
REDIS_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == REDIS_OK))
{
redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",
sp->changes, (int)sp->seconds);
rdbSaveBackground(server.rdb_filename);
break;
}
}
}写多读少的场景下,使用 RDB 备份的风险
Ref
解决方法:全量快照后只做增量快照,但是需要记住修改的数据,下次全量快照时再写入,但这需要在内存中记录修改的数据。因此 Redis 4.0 提出了混合使用 AOF 和全量快照,用
aof-use-rdb-preamble yes
设置。这样,两次全量快照间的修改会记录到 AOF 文件
- 全量数据写入磁盘,磁盘压力大。快照太频繁,前一个任务还未执行完,快照任务之间竞争磁盘带宽,恶性循环
- fork 操作本身阻塞主线程,主线程内存越大,阻塞时间越长,因为要拷贝内存页表
频繁执行全量快照的问题
利用 COW 机制,fork 出子进程共享主线程的内存数据。在主线程修改数据时把这块数据复制一份,此时子进程将副本写入 rdb,主线程仍然修改原来的数据
如何保证写操作正常执行
value 结构
备注
示例
类型
编码,值
表示可以用 8 位整数表示的字符串
REDIS_RDB_ENC_INT8,123
REDIS_RDB_TYPE_STRING
表示字符串
REDIS_ENCODING_RAW, 5, hello
元素个数,列表元素
其中会记录每个元素的长度
3, 5, "hello", 5, "world"
REDIS_RDB_TYPE_LIST
元素个数,集合元素
其中会记录每个元素的长度
3, 5, "hello", 5, "world"
REDIS_RDB_TYPE_SET
键值对个数,键值对
其中会记录每个键值对 key, value 的长度
2, 1, "a", 5, "apple", 1, "b", 6, "banana"
REDIS_RDB_TYPE_HASH
元素个数,member 和 score 对
其中会记录 member 的长度,member 在 score 前面
2, 2, "pi", 4, "3.14", 1, "e", 3, "2.7"
REDIS_RDB_TYPE_ZSET
转化成字符串对象的整数集合
读取 RDB 时需要将字符串对象转化回整数集合
REDIS_RDB_TYPE_SET_INTSET
转化成字符串对象的压缩列表
读取时需要转化成列表
REDIS_RDB_TYPE_LIST_ZIPLIST
转化成字符串对象的压缩列表
读取时需要转化成哈希
REDIS_RDB_TYPE_HASH_ZIPLIST
转化成字符串对象的压缩列表
读取时需要转化成有序集合
REDIS_RDB_TYPE_ZSET_ZIPLIST
各类型对应的 value 结构如下:
这个类型会影响读取数据时如何解释后面 value 代表的值,而 key 则总是被当作 REDIS_RDB_TYPE_STRING 类型
这些编码常量所对应的值都可以在 rdb.h 中查看
数据结构类型
编码常量
字符串
REDIS_RDB_TYPE_STRING,值为 0
列表
REDIS_RDB_TYPE_LIST,值为 1
集合
REDIS_RDB_TYPE_SET,值为 2
有序集和
REDIS_RDB_TYPE_ZSET,值为 3
哈希
REDIS_RDB_TYPE_HASH,值为 4
使用压缩列表实现的列表
REDIS_RDB_TYPE_LIST_ZIPLIST
使用整数集合实现的集合
REDIS_RDB_TYPE_SET_INTSET
使用压缩列表实现的有序集合
REDIS_RDB_TYPE_ZSET_ZIPLIST
使用压缩列表实现的哈希
REDIS_RDB_TYPE_HASH_ZIPLIST
数据文件中存储了所有的数据。开头的 SELECTDB 常量(值为 376)和紧接着的编号,指示了读取 RDB 文件时,后续加载的数据将会被写入哪个数据库中。
RDB 文件格式(以版本“0006”为例)
Redis 使用
int serverCron
函数执行定时任务,这些任务包括自动保存条件检查、更新时间戳、更新 LRU 时钟等。serverCron
每隔 100 ms 执行一次,其中检查自动保存条件的代码如下:[](https://github.com/redis/redi...
struct redisServer
中long long dirty
用来保存从上一次 RDB 持久化之后数据库修改的次数,set <key> <value>
会对 dirty 加一,而sadd <set-name> <value1> <value2> <value3>
会对 dirty 加 3。time_t lastsave
记录了上一次完成 RDB 持久化的时间如何判断是否满足自动保存的条件?
Redis 启动式根据用户设定的保存条件开启自动保存。在
/etc/redis/redis.conf
配置文件中加上save <seconds> <changes>
表示在 seconds 秒内对数据库进行了 changes 次修改,BGSAVE
命令就会执行。这个配置会被加载到struct redisServer
的struct saveparam
参数中。saveparam
是一个链表,当配置多个save
条件时,这个条件都会被加入链表中。自动执行持久化
载入 RDB 文件时实际工作由
rdb.c/rdbLoad
完成,载入期间主线程处于阻塞状态。
SAVE
和BGSAVE
都会调用rdb.c/rdbSave
执行真正的持久化过程。在 Redis 6.0 以前,虽然 Redis 处理处理请求是单线程的,但 Redis Server 还有其他线程在后台工作,例如 AOF 每秒刷盘、异步关闭文件描述符这些操作Redis 的 RDB 持久化功能通过
SAVE
和BGSAVE
两个命令可以生成压缩的二进制 RDB 文件,通过这个文件可以还原生成文件时数据库的状态。手动执行持久化
RDB
AOF 重写完成后,子进程会向父进程发送一个完成信号。父进程收到后将 AOF 重写区的内容追加到新 AOF 文件中,然后将 AOF 改名,覆盖原来的 AOF 文件
AOF 缓冲
对于有多个元素的 key,例如大列表、大集合,简单的将所有元素的写入合并到一条语句中可能会形成一条过大的写入语句,在后续执行命令时导致客户端输入缓冲区溢出。因此 Redis 配置了一个
REDIS_AOF_REWRITE_ITEMS_PER_CMD
常量,当一条命令中的元素超过这个数量时,会被拆分成多条语句AOF 重写的过程中并不会读取原有的 AOF 文件,而是直接根据数据库当前的状态生成一份新的 AOF 文件,类似于 SQL 导出数据时直接生成 INSERT 语句。
由于 AOF 文件是依次记录客户端发来的写入命令,在写入较多的情况下,AOF 文件会快速膨胀,因此需要 AOF 重写精简其中的命令。
AOF 重写
AOF 文件载入
flushAppendOnlyFile 行为
appndfsync 选项
总是将 aof_buf 缓冲区中的内容写入内存缓冲区,并同步到 AOF 文件
always
将 aof_buf 缓冲区中的内容写入内存缓冲区,如果距离上一次同步超过一秒,则同步到 AOF 文件
everysec
只写入到内存缓冲区,由 OS 后续决定何时同步到 AOF 文件
no
flushAppendOnlyFile
中根据配置文件中的 appendfsync 参数判断是否写入 AOF 文件。将 aof_buf 中的命令写入 AOF 文件分为两个步骤:AOF 写入条件判断规则
- 服务器在执行完命令后,会将命令写入到
struct redisServer
的sds aof_buf
` 缓冲区末尾- Redis 进程每一次事件循环(处理客户端请求的循环)末尾都会调用
void flushAppendOnlyFile
检查时候需要将缓冲区中的命令写入 AOF 文件AOF 持久化执行步骤
- 刚执行完命令,还没写入,此时宕机,这个命令和相应的数据有丢失的风险
- 避免了当前命令的阻塞,但是可能阻塞下一个命令
风险:
- 可以保证日志中记录的命令都是正确的
- 命令执行后才记录到日志,不会阻塞当前写操作
AOF 是写后日志,与写前日志(Write Ahead Log, WAL)相反,写入命令执行完成后才会记录到 AOF 日志。这样设计是因为 AOF 记录的是接收到的命令,并且记录时不会进行语法检查(保证性能),使用写后日志有 2 个优点:
命令执行完成后才会写入 AOF 日志
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。