Redis持久化机制详解
Redis持久化机制详解
前言
Redis提供两种持久化机制:RDB和AOF,用于将内存中的数据保存到磁盘。
- RDB:定时将所有键值对以二进制压缩数据形式写入rdb文件,是数据库状态的快照,属于全量备份
- AOF:将Redis执行的写入命令按协议格式以纯文本形式记录到AOF文件中,属于增量备份
RDB持久化机制
rdb机制会定时将所有键值对,以二进制压缩数据的形式写入rdb文件中,其实就是给当前数据库状态做了一个全量备份,恢复数据的时候,只需要将rdb文件加载到内存中即可,优点是rdb文件采用了二进制压缩,文件体积较小,恢复速度快,所以rdb机制适合大规模的数据备份和恢复,但是由于是定时写入rdb文件,如果在两次写入期间redis发生了宕机的话,就会造成最后一次rdb之后的数据全部丢失,适合于对于数据完整性要求不高的业务场景
1.1 RDB文件的创建与载入
创建RDB文件的命令
SAVE命令
- 阻塞redis服务器进程,直到rdb文件创建完毕
- 在阻塞期间,redis服务器进程不能处理任何命令请求
BGSAVE命令
- fork出子进程负责创建rdb文件
- 父进程继续处理命令请求,不会阻塞
- 写时复制:fork出的子进程和父进程共享相同的内存数据,当父进程需要写入或者修改数据的时候,会将内存数据复制出一个副本,然后在这个副本上进行修改,避免了子进程和父进程由于共享内存数据导致的数据安全问题,所以子进程创建的rdb文件只会保存fork出子进程那一刻的数据库状态
- 子进程先把内存中的数据写入到一个临时文件中,写入完毕之后,再用这个临时文件取代原来的rdb文件
实现原理
def SAVE():
rdbSave() # 创建RDB文件
def BGSAVE():
pid = fork() # 创建子进程
if pid == 0:
rdbSave() # 子进程负责创建RDB文件
signal_parent() # 完成后向父进程发送信号
elif pid > 0:
handle_request_and_wait_signal() # 父进程继续处理请求
else:
handle_fork_error() # 处理出错情况
RDB文件载入
- 服务器启动时自动将rdb文件加载到内存中
- 因为是程序自动完成的,所以redis没有提供专门的载入命令
- 服务器启动时检测到RDB文件存在,就会自动载入
1.2 定期保存到rdb文件
保存条件设置
通过save
选项设置保存条件,默认配置:
save 900 1 # 900秒内至少1次修改
save 300 10 # 300秒内至少10次修改
save 60 10000 # 60秒内至少10000次修改
服务器状态结构
struct redisServer {
struct saveparam *saveparams; // 保存条件数组
long long dirty; // 修改计数器
time_t lastsave; // 上次保存时间
};
struct saveparam {
time_t seconds; // 秒数
int changes; // 修改数
};
检查机制
serverCron
函数每100毫秒执行一次- 遍历所有保存条件,满足任一条件就执行BGSAVE
def serverCron():
for saveparam in server.saveparams:
save_interval = unixtime_now() - server.lastsave
if server.dirty >= saveparam.changes and save_interval > saveparam.seconds:
BGSAVE()
AOF持久化机制
aof机制会将redis所执行的写入命令,按照redis请求协议的格式,以纯文本的形式追加到aof文件末尾,优点是最多丢失一秒的数据,更能保证数据的安全性,缺点是aof文件的体积较大,需要定时重写,所以aof适合于对数据完整性和一致性要求特别高的业务
2.1 AOF持久化的实现步骤
AOF持久化分为三个步骤:命令追加、文件写入、文件同步
2.1.1 命令追加
- 当AOF功能开启时,服务器执行写命令后,会将命令以协议格式追加到
aof_buf
缓冲区
struct redisServer {
sds aof_buf; // AOF缓冲区
};
示例:
执行
SET KEY VALUE
后,追加内容:*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
2.1.2 文件写入与同步
在每个事件循环结束前,调用flushAppendOnlyFile
函数:
def eventLoop():
while True:
processFileEvents() # 处理文件事件
processTimeEvents() # 处理时间事件
flushAppendOnlyFile() # 考虑写入AOF文件
appendfsync选项
选项值 | 行为描述 | 效率 | 安全性 |
---|---|---|---|
always | 每次事件循环都写入并同步 | 最慢 | 最安全,最多丢失一个事件循环的数据 |
everysec | 每次写入,每秒同步一次 | 较快 | 较安全,最多丢失一秒数据 |
no | 每次写入,同步由操作系统决定 | 最快 | 安全性最低 |
2.2 AOF文件重写
2.2.1 重写的必要性
随着服务器运行,AOF文件会越来越大,其中就包含了很多对于恢复数据库状态来说是不必要的命令,所以要定时根据当前的数据库状态,重写aof文件,只保留恢复当前数据库状态的所必须的最少命令
2.2.2 重写实现原理
不需要根据现有AOF文件来重写,而是直接按照当前的数据库状态来编写命令
例如,原来aof文件中的针对同一个key的多条写入命令就完全可以简化成一条命令,只要保证命令执行之后得到的结果是一样的就可以了
# 原AOF文件可能有多条命令
RPUSH list "A" "B"
RPUSH list "C"
LPOP list
RPUSH list "D"
# 重写后只需一条命令
RPUSH list "B" "C" "D"
2.2.3 重写算法
def aof_rewrite(new_aof_file_name):
f = create_file(new_aof_file_name)
for db in redisServer.db:
if db.is_empty(): continue
f.write_command("SELECT", db.id)
for key in db:
if key.is_expired(): continue
# 根据键类型重写
if key.type == String:
rewrite_string(key)
elif key.type == List:
rewrite_list(key)
# ... 其他类型
# 重写过期时间
if key.have_expire_time():
rewrite_expire_time(key)
f.close()
2.2.4 后台重写(BGREWRITEAOF)
由于aof重写的频率是比较高的,所以应该要在后台进行,避免阻塞redis服务器进程,但是如果重写期间服务器继续处理命令,就会出现数据不一致的问题,这个时候就可以让父进程将新命令同时写入aof_buf缓冲区和aof重写缓冲区
解决方案:AOF重写缓冲区
- 子进程执行重写时,父进程将新命令同时写入:
- AOF缓冲区(正常AOF文件)
- AOF重写缓冲区
重写流程:
- 创建子进程执行重写
- 父进程继续处理命令,同时写入两个缓冲区
- 子进程完成重写后发送信号
- 父进程将重写缓冲区内容追加到新AOF文件
- 原子性替换旧AOF文件
RDB vs AOF对比
特性 | RDB | AOF |
---|---|---|
文件大小 | 小(二进制压缩) | 大(纯文本命令) |
恢复速度 | 快 | 慢 |
数据安全性 | 可能丢失两次保存间的数据 | 最多丢失1秒数据 |
对性能影响 | BGSAVE对性能影响小 | 写入频繁时影响较大 |
备份类型 | 全量备份 | 增量备份 |