Redis 持久化

Redis 是内存数据库,数据都存储在内存中,如果没有持久化机制,一旦服务器进程退出数据就会丢失。所以 Redis 提供了两种持久化的机制:保存数据库中键值对的 RDB (Redis DataBase)和保存执行的写命令的 AOF(Append Only File)。

RDB

RDB 文件结构

启动 Redis(3.0.0 版本)服务器,在没有任何键值对的情况下,使用 SAVE 命令生成 dump.rdb 文件,然后使用 Linux 命令 xxd 查看文件内容,然后使用 SEXEX 命令设置一个字符串键,再重新 SAVE 一下:

xxd dump.rdb

# 1. 不包含任何键值对
# 00000000: 5245 4449 5330 3030 36ff dcb3 43f0 5adc REDIS0006...C.Z.
# 00000010: f256 .V

# 2. 执行: SEXEX key 1000 value
# 00000000: 5245 4449 5330 3030 36fe 00fc 04cd c9a6 REDIS0006.......
# 00000010: 6b01 0000 0003 6b65 7905 7661 6c75 65ff k.....key.value.
# 00000020: 3a8f 5f99 f12d 5c10 :._..-\.

RDB(dump.rdb)文件主要分为 5 部分:开始是 5 个字节的字符串 REDIS(ASCII 码是 0x5245444953);紧接着是 4 个字节的版本号(db_version),在这里版本号是字符串 0006 (ASCII 码是 0x0x30303036);databases 部分包含 0 个或任意多个数据库的数据,如果所有数据库都为空,这个部分也为空;最后是标识数据结束的 1 个字节 EOF(恒为 0xff)和 8 个字节的校验和(check_sum),每次写入或读取新数据时都会重新计算更新内存中的校验和。

REDIS db_version databases EOF check_sum
0x5245444953 0x30303036 不定 0xff 8 字节

执行 SEXEX 命令设置一个字符串键后,databases 部分就有一个数据库的数据了,主要由 3 部分构成:最开始的是 1 个字节的常量 SELECTDB(恒为 0xfe),当读入程序遇到这个值的时候就知道接下来要读入的将是一个数据库号码;接来下就是数据库号码 db_number,长度可以是 1 或 2 或 5 个字节,在这里是 1 个字节 0x00 表示 0 号数据库,当程序读入 db_number 后会调用 SELECT 命令切换到对应的数据库;最后是实际的键值对数据。

SELECTDB db_number key_value_pairs
0xfe 1/2/5 字节 不定

key_value_pairs 保存的键值对数据主要由 5 部分都构成:有过期时间时才会有 1 字节的 EXPIRETIME 常量(恒为 0xfc)和 8 字节表示过期时间的 ms 字段,在这里 ms 字段的值是 0x0000016ba6c9cd04,也就是 10 进制的 1561871371524(北京时间 2019-06-30 13:09:31);接下来一个字节表示键值对的类型,这里的 0x00 即 REDIS_RDB_TYPE_STRING 表示字符串键,紧接着的 1 个字节 0x03 表示键的长度,然后是表示键的字符串 key (ASCII 码是 0x6b6579),接下来的 1 个字节 0x05 表示值的长度,最后是表示值的字符串 value (ASCII 码是 0x76616c7565)。

EXPIRETIME ms TYPE key value
0xfc 8 字节 1 字节 不定 不定

当然,上面只分析了字符串键的一个简单例子,其他不同类型的键会采用不同的编码,也会视情况使用压缩,这些细节就不一一讨论了。

创建与载入

Redis 的 SAVE 和 BGSAVE 命令都可以用于生成 RDB 文件:SAVE 命令会阻塞 Redis 服务器进程直到 RDB 文件创建完毕为止,这期间服务器不能处理任何命令请求;BGSAVE 命令会派生出一个子进程,然后由子进程负责创建 RDB 文件,服务器进程(父进程)可以继续处理其他命令请求。

// redis.h
struct redisServer {
...
// 记录了保存条件的数组
struct saveparam *saveparams;

// 修改计数器
long long dirty;

// 上一次至此那个保存的时间
time_t lastsave;
...
};

struct saveparam {

// 秒数
time_t seconds;

// 修改数
int changes;

};

因为 BGSAVE 命令可以在不阻塞服务器进程的情况下执行,所以 Redis 允许用户通过设置服务器配置的 save 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令。Redis 维持了一个 saveparams 数组、 dirty 计数器和 lastsave 属性,用来实现这种自动间隔性保存:dirty 计数器记录距离上一次成功执行 SAVE 命令或者 BGSAVE 命令之后,服务器对数据库(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作);lastsave 属性是一个 UNIX 时间戳,记录了服务器上次成功执行 SAVE 命令或 BGSAVE 命令的时间。

当 Redis 服务启动时,用户可以通过制定配置文件或者传入启动参数的方式设置 save 选项,如果用户没有主动设置 save 选项,那么服务器会为 save 选项设置默认条件(多少秒内至少修改多少次):

save 900 1
save 300 10
save 60 10000

Redis 的自动间隔保存也是在周期任务 serverCron 函数中执行的,该函数每次运行时都会检查 save 选项设置的保存条件是否已经满足,如果满足的话就执行 BGSAVE 命令。最后,服务器在载入 RDB 文件期间,会一直处于阻塞状态直到载入工作完成为止。

AOF

被写入 AOF 文件的所有命令都是以 Redis 的命令请求协议格式保存的,可以直接打开 AOF 文件查看:

# 1. 在配置文件中打开 AOF
appendonly yes

# 2. 执行: SET key value
*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue

AOF 实现

AOF 持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)3 个步骤。服务器在执行完命令之后,会将对应的命令以 Redis 的命令请求格式追加到 aof_buf 缓冲区的末尾。Redis 服务器进程就是一个事件循环(Loop),在每次结束一个事件循环之前都会调用 flushAppendOnlyFile 函数,考虑是否将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里面。

// redis.h
struct redisServer {
...
// AOF 缓冲区
sds aof_buf;

// AOF 重写缓存链表,链接着多个缓存块
list *aof_rewrite_buf_blocks;
...
};

flushAppendOnlyFile 函数的行为由 appendFsync 配置项决定,配置项不同的值也是对 AOF 持久化的效率和安全性的权衡。为了提高文件写入效率,用户调用 write 函数将数据写入文件时,操作系统通常将写入数据暂时保存在一个内存缓冲区中,等到缓冲区满或超过一定时间后才会真正将数据写入磁盘;为了确保写入数据的安全性,也提供了 fsync 和 fdatasync 两个同步函数,强制操作系统立即将缓冲区中的数据写入到硬盘。

appendFsync flushAppendOnlyFile 行为
always 将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件
everysec 写入 AOF 文件,如果距上次同步时间超过 1 秒,创建一个单独的线程同步 AOF 文件
no 写入 AOF 文件,不强制同步 AOF 文件,何时同步由操作系统决定

因为 AOF 文件中包含了重建数据库所需要的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件中保存的所有命令,就可以还原服务器关闭之前的数据库数据。

AOF 重写

因为 AOF 持久化是通过保存被执行的所有写命令实现的,所以随着时间流逝,执行的写命令越来越多,AOF 文件的体积也会不断膨胀。为了解决 AOF 文件膨胀的问题,Redis 提供了 AOF 文件重写(Rewrite)机制,通过读取服务器当前的键值对数据生成新的 AOF 文件,替换掉旧的 AOF 文件,或许叫 AOF 「替换」会更准确一些。

AOF 重写在 aof_rewrite 函数中实现,首先从数据库中读取键现在的值,然后根据键的类型用一条命令去记录键值对,代替之前记录这个键值对的多条命令,从而达到去除冗余命令的效果,缩减 AOF 文件的体积。为了不影响服务器正常处理命令请求,一般使用 BGREWRITEAOF 命令进行 AOF 重写,即创建一个子进程执行 aof_rewrite 函数。子进程带有服务器进程的数据副本,但是在执行 AOF 重写期间,服务器进程还在继续处理命令请求变更数据库数据,导致数据库的数据和子进程重写的 AOF 文件不一致,为了解决这个问题引入了 AOF 重写缓冲区:

  • 从创建子进程开始,服务器执行的所有写命令都会被记录到 AOF 重写缓冲区里面;
  • 子进程重写完成后发信号告诉服务器进程,服务器进程将 AOF 重写缓冲区中内容写入新的 AOF 文件;
  • 最后对新的 AOF 文件进行改名,原子地覆盖现有的 AOF 文件,完成 AOF 重写。
0%