Redis 持久化相关源码阅读笔记,源码文件 rdb.h
& rdb.c
& aof.c
。
Redis 提供两种持久化方式供选择:RDB 和 AOF,其详细介绍可阅读 Redis Persistence。
1. RDB
RDB 为数据集在某一时间点的 snapshot,可通过 BGSAVE 或 SAVE 命令主动触发,也同通过配置 save 相关参数被动触发达到备份数据的目的。除此之外,Redis 的主从全量同步时也会生成 rdb 文件以供同步使用。
1.1. RDB 编码格式
在 RDB 文件中,每种数据类型都会按照 <type> <len> <data>
的顺序编码,当 type
为某些类型时,会省略 data
字段。type
字段长度固定为 1 byte (0-255),值得注意的是,其不仅涵盖不同的数据对象类型,还包含多种 RDB opcodes。这些 RDB opcodes 类型用于在编码(解码)时存储(加载)除了数据对象内容外的额外信息,以备份(恢复)数据库的完整状态。
1.1.1. Object types
从 rdbSaveObjectType
函数中我们可以知道不同的数据对象类型与 RDB type 的对应关系如下表所示:
robj->type | robj->encoding | RDB type |
---|---|---|
OBJ_STRING | All | RDB_TYPE_STRING |
OBJ_LIST | OBJ_ENCODING_QUICKLIST | RDB_TYPE_LIST_QUICKLIST |
OBJ_SET | OBJ_ENCODING_INTSET | RDB_TYPE_SET_INTSET |
OBJ_ENCODING_HT | RDB_TYPE_SET | |
OBJ_ZSET | OBJ_ENCODING_ZIPLIST | RDB_TYPE_ZSET_ZIPLIST |
OBJ_ENCODING_SKIPLIST | RDB_TYPE_ZSET_2 | |
OBJ_HASH | OBJ_ENCODING_ZIPLIST | RDB_TYPE_HASH_ZIPLIST |
OBJ_ENCODING_HT | RDB_TYPE_HASH | |
OBJ_STREAM | All | RDB_TYPE_STREAM_LISTPACKS |
OBJ_MODULE | All | RDB_TYPE_MODULE_2 |
RDB_TYPE_ZSET
和 RDB_TYPE_MODULE
目前在编码时已不再使用,仅供解码时使用,以便能兼容之前版本的 RDB。容易看出高版本的 Redis 可以解码低版本的 RDB 文件,而低版本的 Redis 不可解码高版本的 RDB 文件。
从 rdbSaveKeyValuePair
函数中我们可以看到,在存储数据对象内容时会按照 <RDB type> <key> <value>
的顺序排布,其中 key
和字符串对象值的存储格式一致,均调用 rdbSaveStringObject
进行存储。
从 rdbSaveObject
中我们可以发现 value
的存储格式与数据对象的类型(RDB type)有关,其对应关系如下所示:
RDB type | RDB content (不考虑压缩) |
---|---|
RDB_TYPE_STRING | <len> [data] |
RDB_TYPE_LIST_QUICKLIST | <ql->len> <zlbytes> [zdata] ...... |
RDB_TYPE_SET_INTSET | <intsetBytes> [intset.data] |
RDB_TYPE_SET | <dictSize> <ele.len> [ele.data] ...... |
RDB_TYPE_ZSET_ZIPLIST | <zlbytes> [zdata] |
RDB_TYPE_ZSET | 兼容旧版 RDB,已弃用 |
RDB_TYPE_ZSET_2 | <zsl->length> <member.len> [member.data] <score> ...... |
RDB_TYPE_HASH_ZIPLIST | <zlbytes> [zdata] |
RDB_TYPE_HASH | <dictSize> <field.len> [field.data] <value.len> [value.data] ...... |
RDB_TYPE_STREAM_LISTPACKS | TODO |
RDB_TYPE_MODULE | 兼容旧版 RDB,已弃用 |
RDB_TYPE_MODULE_2 | TODO |
<>
表示该项必定存在,[]
表示该项可能不存在
RDB_TYPE_STRING
|
|
字符串对象调用 rdbSaveStringObject
进行存储,其编码会在以下情况下省略 data
字段:
robj->encoding
为OBJ_ENCODING_INT
且数值范围为 $[2^{-31}, 2^{31}-1]$ (int32_t
)- 字符串长度小于 11 且可转化为
int32_t
范围内的整型
通过阅读 rdbSaveRawString
可以发现,当使用 <len> <data>
存储字符串对象时,若 rdbcompression 配置为 yes,且字符串长度大于 20,则会尝试对字符串进行 LZF 压缩,若压缩后最小可节省 4 byte,则调用 rdbSaveLzfBlob
存储压缩后的内容:
|
|
OBJ_ENCODING_QUICKLIST
首先存储 ql->len
,即 quicklist 中节点的个数;然后遍历 quicklist,如果当前 quicklistNode
已被压缩,则调用 rdbSaveLzfBlob
存储压缩后的 ziplist
,否则调用 rdbSaveRawString
存储 ziplist
。可以看出 ziplist
被当成字符串直接存储,便于存取。
RDB_TYPE_SET
当集合类型使用 dict
作为底层实现时,其对应的 RDB type 为 RDB_TYPE_SET
。在 RDB_TYPE_SET
中,首先存储集合大小 dictSize
;随后遍历 dict
,调用 rdbSaveRawString
存储 dictEntry->key
(集合类型中 dictEntry->val
为空)。
RDB_TYPE_SET_INTSET
当集合类型使用 intset
作为底层实现时,其对应的 RDB type 为 RDB_TYPE_SET_INTSET
,调用 rdbSaveRawString
存储整个 intset
。
RDB_TYPE_ZSET_ZIPLIST
当 ZSET 使用 ziplist
作为底层实现时,其对应的 RDB type 为 RDB_TYPE_ZSET_ZIPLIST
,调用 rdbSaveRawString
存储整个 ziplist
。
RDB_TYPE_ZSET_2
当 ZSET 使用 skiplist 作为底层实现时,其对应的 RDB type 为 RDB_TYPE_ZSET_2
。首先存储 skiplist 中的节点个数 zskiplist->length
,随后逆序遍历 skiplist,调用 rdbSaveRawString
存储 zskiplistNode->ele
;调用 rdbSaveBinaryDoubleValue
存储 zskiplistNode->score
。逆序遍历的目的是为了在 RDB load 阶段便于 skiplist 的插入操作,由于每个将要插入的节点都小于当前最小的节点,因此只需往头部插入即可。
RDB_TYPE_HASH_ZIPLIST
当 Hash 使用 ziplist
作为底层实现时,其对应的 RDB type 为 RDB_TYPE_HASH_ZIPLIST
,调用 rdbSaveRawString
存储整个 ziplist
。
RDB_TYPE_HASH
当 Hash 使用 dict
作为底层实现时,其对应的 RDB type 为 RDB_TYPE_HASH
。首先存储哈希表大小 dictSize
;随后遍历 dict
,调用两次 rdbSaveRawString
按序存储 dictEntry->key
和 dictEntry->v.val
。
RDB_TYPE_STREAM_LISTPACKS
TODO
RDB_TYPE_MODULE_2
TODO
1.1.2. RDB opcodes
RDB type | Meaning | RDB content |
---|---|---|
RDB_OPCODE_MODULE_AUX | Module auxiliary data | TODO |
RDB_OPCODE_IDLE | LRU idle time | len: <idletime> (seconds) |
RDB_OPCODE_FREQ | LFU frequency | Raw: <LFUCounter> (8-bit) |
RDB_OPCODE_AUX | RDB aux field | Str: <key.len> [key.data] <val.len> [val.data] |
RDB_OPCODE_RESIZEDB | Hash table resize hint | len: <db_size> <expires_size> |
RDB_OPCODE_EXPIRETIME_MS | Expire time in milliseconds | Raw: <expiretime> |
RDB_OPCODE_EXPIRETIME | Old expire time in seconds | 兼容旧版 RDB,已弃用 |
RDB_OPCODE_SELECTDB | DB number of the following keys | len: <current_db_id> |
RDB_OPCODE_EOF | End of the RDB file | Raw: <cksum> |
- 省略 redis module sub opcodes
- len 表示该字段内容与
len
字段编码规则相同,将在下节讲述 - Raw 表示该字段内容定长,直接写入存储
- Str 表示该字段调用
rdbSaveRawString
写入
1.2. length 编码
Redis 为了节省存储空间,不仅会对 data
字段进行压缩,而且也会使用不定长空间存储 len
字段。通过读取 len
字段首个字节可知道此时的编码格式。
长度信息
通过阅读 rdbSaveLen
可以发现,在使用 len
字段存储长度信息时,其原生值为整型,根据整型值的大小可用以下四种编码方式之一进行存储:
00|XXXXXX
: the len is the 6 bits of this byte01|XXXXXX XXXXXXXX
: the len is 14 byes, 6 bits + 8 bits of next byte10|000000 <32 bit integer>
: A full 32 bit len in net byte order will follow10|000001 <64 bit integer>
: A full 64 bit len in net byte order will follow
字符串对象编码
正如之前所说,在存储字符串对象时,一般会使用 len
字段存储字符串长度,data
字段存储字符串内容,但在字符串对象可用整型表示时或可压缩节省空间时,len
字段所存储的信息便不是字符串长度信息,通过阅读 rdbEncodeInteger
,可以发现根据 len
字段首字节我们可以知道字符串对象此时的编码方式:
11|000000 <8 bit integer>
: 字符串对象可用 8 bit 整型表示11|000001 <16 bit integer>
: 字符串对象可用 16 bit 整型表示11|000010 <32 bit integer>
: 字符串对象可用 32 bit 整型表示11|000011 <compress_len> <original_len> <compress_data>
: 字符串对象可压缩
1.3. RDB 文件内容排布
通过阅读 rdbSaveRio
,我们可以知道 RDB 文件内容的排布方式:
-
RDB 版本号:格式为
REDIS0009
,其中 $9$ 为Redis-6.0.8 的 RDB_VERSION,直接写入二进制串无编码 -
Aux Fields: 编码格式
<RDB_OPCODE_AUX> <key.len> [key.data] <val.len> [val.data]
,key-val 对内容如下:key value meaning redis-ver
REDIS_VERSION
redis 当前版本 redis-bits
sizeof(void*)
处理器运算位数 ctime
time(NULL)
创建时间 used-mem
zmalloc_used_memory()
实际分配内存大小 repl-stream-db
rsi->repl_stream_db
DB to select in server.master client repl-id
server.replid
My current replication ID repl-offset
server.master_repl_offset
My current replication offset aof-preamble
0 or 1 Load/save the RDB as AOF preamble? 其中:
- 若传入的
rdbSaveInfo
(rsi) 为空,则无须存储repl-stream-db
、repl-id
和repl-offset
信息。 aof-preamble
为 1 时,表示该 rdb 文件是 AOF 重写时生成的,可通过配置项 aof-use-rdb-preamble 修改该值。
- 若传入的
-
Module-specific aux values: Iterate over modules, and trigger rdb aux saving for the ones modules types who asked for it. (when is
REDISMODULE_AUX_BEFORE_RDB
) -
遍历及存储每个
redisDb
的具体信息:-
db 编号:
<RDB_OPCODE_SELECTDB> <current_db_id>
-
db 大小:
RDB_OPCODE_RESIZEDB <db_size> <expires_size>
-
遍历
redisDb->dict
,存储数据对象相关信息:-
如有过期时间,则存储
<RDB_OPCODE_EXPIRETIME_MS> <expiretime>
expiretime
is the absolute unix time in milliseconds
-
按照内存管理策略存储 LRU/LFU 信息:
- LRU:
<RDB_OPCODE_IDLE> <idletime>
- LFU:
<RDB_OPCODE_FREQ> <LFUCounter>
- LRU:
-
数据对象内容:
<type> <key> <value>
,详细内容可参照 Object types
-
-
-
Lua scripts: 若传入的
rdbSaveInfo
不为空,则遍历及存储此时缓存的 lua 脚本信息,每个 lua 脚本的编码格式为<RDB_OPCODE_AUX> <lua> <lua-script>
-
Module-specific aux values: Iterate over modules, and trigger rdb aux saving for the ones modules types who asked for it. (when is
REDISMODULE_AUX_AFTER_RDB
) -
EOF opcode:
<RDB_OPCODE_EOF> <cksum>
,若未启用计算校验和,则cksum
值为 0
1.4. RDB 相关命令
SAVE
TODO
BGSAVE
TODO
2. AOF
Redis 除了使用数据集的 snapshot (RDB) 提供持久化保证之外,还会使用 Append Only Flie(AOF) 记录写请求。
2.1. 写入 AOF 缓冲区
在 Redis 调用 propagate
函数将请求同步至 AOF 和 replications 时,会调用 feedAppendOnlyFile 函数将请求写入 AOF 缓冲区,该函数的接口如下:
|
|
其执行流程大致为:
- 如传入的
dictid
和上次记录请求时的 db 不同,则写入SELECT dictid
请求以切换 db - 命令转换,将过期相对时间改为绝对时间:
- Translate
EXPIRE
/PEXPIRE
/EXPIREAT
intoPEXPIREAT
- Translate
SETEX
/PSETEX
toSET
andPEXPIREAT
- Translate
SET [EX seconds][PX milliseconds]
toSET
andPEXPIREAT
- Translate
- 将内容追加至 AOF 缓冲区
server.aof_buf
- 如此时有子进程正在执行 AOF rewrite,则会将内容追加之 AOF rewrite buff
server.aof_rewrite_buf_blocks
2.2. 写入 AOF 文件
AOF 缓冲区的数据会在以下几种情况下调用 flushAppendOnlyFile
写入 AOF 文件,同时可选择是否启用强制模式:
- 使用
CONFIG
命令关闭 AOF 功能时,模式为强制模式 - 在服务器定期执行
serverCron
时,发现此前有被推迟执行的写入 AOF 文件操作时,模式为非强制模式,执行频率为server.hz
- 在服务器定期执行
serverCron
时,发现此前写入 AOF 文件有错误发生时,模式为非强制模式,执行频率为 $1$ - 关闭服务器之前,模式为强制模式
- 因为要求在返回客户端响应之前将内容写入 AOF 文件,因此我们需在进入 event loop 之前的 beforeSleep 函数中调用
flushAppendOnlyFile
写入 AOF 文件,模式为非强制模式。 (主要情形)
函数 flushAppendOnlyFile
的接口如下:
|
|
阅读 Redis persistence demystified 后我们知道,简单来说:
- 当调用
write()
后,此时数据在 kernel’s buffer - 继续调用
fsync()
后,数据在 disk cache (也可选择让 OS 自行控制何时 flush) - 随后由 disk controller 将数据写入物理介质
其中 1 2 可控,而 flushAppendOnlyFile
主要也可分为这两部分。
write
- 在使用强制模式时,如果此时 AOF 缓冲区非空,则一定会调用
write()
,即使可能阻塞 Redis,因此使用CONFIG
命令关闭 AOF 功能可能会阻塞 Redis - 只有当
fsync
的策略为everysec
时,非强制模式才会在下述情况下阻止调用write()
:- 当前有 background fsync 进程在运行,且此前无推迟的
write
调用,记录本次推迟的write
调用时间并返回 - 当前有 background fsync 进程在运行,且此前推迟的
write
调用在 $2s$ 内,则直接返回若超过 2s,则不会阻止调用
write
,此时可能会阻塞 Redis
- 当前有 background fsync 进程在运行,且此前无推迟的
fsync
AOF 调用 fsync()
的频率可通过配置 appendfsync 进行控制,可选项有三:
no
: don’t fsync, just let the OS flush the data when it wants. Faster.always
: fsync after every write to the append only log. Slow, Safest.everysec
: fsync only one time every second. Compromise.
通过在 write
阶段的介绍可知,当使用 everysec
策略时,事实上可能会丢失 $2s$ 的写入,而不是 $1s$。
如果有 RDB saving 或 AOF rewriting 子进程,可能会消耗 IO 资源,导致 fsync
操作耗时过长阻塞 Redis,因此为性能考虑可以通过配置 no-appendfsync-on-rewrite 为 yes
阻止调用 fsync
。
2.3. AOF Rewrite
由于 AOF 文件的 Append Only 要求,会使得该文件大小单调递增,因此 Redis 提供了 AOF rewrite 的功能,使用 rewrite 生成的 AOF 文件替换旧的 AOF 文件,以减小 AOF 文件所占用的磁盘空间大小。既可以主动发起 BGREWRITEAOF 请求要求进行 AOF rewrite,亦可以通过配置 auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 调整自发进行 AOF rewrite 的条件。当当前 AOF 文件大小 server.aof_current_size
大于 auto-aof-rewrite-min-size
且与上次 rewrite 之后 AOF 文件大小 server.aof_rewrite_base_size
相比增长率超过 auto-aof-rewrite-percentage
时,Redis 会自动触发 AOF rewrite 操作 —— 调用 rewriteAppendOnlyFileBackground,其接口如下:
|
|
2.3.1. AOF Rewrite Buffer
由于在 AOF rewrite 执行过程中,Redis 不会停止服务,因此需要记录这期间数据库的变化。Redis 使用双端链表 aof_rewrite_buf_blocks 实现 AOF Rewirte buffer,每个 listNode 指向一个大小为 $10MB$ 的内存块 aofrwblock
:
|
|
如果此时正在进行 AOF rewrite,则 Redis 主进程除了将写请求写入 AOF 缓冲区之外,还会通过调用 aofRewriteBufferAppend将数据写入 AOF rewrite buffer。在 aofRewriteBufferAppend
中,以 append 的形式写入数据,同时会创建 aofChildWriteDiffData
file event,以将 AOF rewrite buffer 中的内容从前往后遍历写入管道 server.aof_pipe_write_data_to_child
,以供子进程读取,写入管道成功后会释放该节点所占用的内存块。因此,AOF rewrite buffer 如同一个先进先出的队列,在 append 时(链表尾部)申请内存,在写入管道后(链表头部)释放内存。
2.3.2. Use RDB preamble
因为 RDB 文件小,加载速度快,因此在 AOF rewrite 时,可使用 RDB 保存当前数据库状态。如果配置项 aof-use-rdb-preamble 为 yes
,则会调用 rdbSaveRio 保存数据库状态,此时 AOF rewrite 结束之后的文件组织形式如下:
|
|
2.3.3. Rewrite Objects
当不启用 aof-use-rdb-preamble
时,会调用 rewriteAppendOnlyFileRio 函数将各个数据对象转换为请求的格式存储。