因为redis是内存数据库,他把数据都存在内存里,所以要想办法实现持久化功能。
RDB
RDB持久化可以手动执行,也可以配置定期执行,可以把某个时间的数据状态保存到RDB文件中,反之,我们可以用RDB文件还原数据库状态。
生成
有两个命令可以生成RDB文件:
- SAVE 命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器,服务器不能接受其他指令。
- BGSAVE 命令由子进程执行保存操作,所以该命令不会阻塞服务器,服务器可以接受其他指令。。
禁止BGSAVE和SAVE同时执行,也就是说执行其中一个就会拒绝另一个,这是为了避免父进程和子进程同时执行两个rdbsave,防止产生竞争条件。
载入
RDB载入工作是服务器启动时自动执行的。
自动保存
用户可以通过save选项设置多个保存条件,服务器状态中会保存所有用 save
选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行 BGSAVE 命令。
比如
save 900 1
save 300 10
满足:服务器在900秒之内被修改至少一次或者300秒内修改至少十次。就会执行BGSAVE。
当服务器启动时,用户可以通过指定配置文件或者传入启动参数来设置save选项,服务器会把条件放到一个结构体里,结构体有一个数组,保存了所有条件。
serverCron函数默认100毫秒检查一次,他会遍历数组依次检查,符合条件就会执行BGSAVE。
RDB文件结构
一个完整 RDB 文件所包含的各个部分:
REDIS,
长度5
字节, 保存着 "REDIS"
五个字符。 通过这五个字符, 可以在载入文件时, 快速检查载入文件是否 RDB 文件。
db_version
,长度 4
字节, 它的值是一个字符串表示的整数, 这个整数记录了 RDB 文件的版本号
databases
部分包含着零个或任意多个数据库, 以及各个数据库中的键值对数据
EOF
常量的长度为 1
字节, 这个常量标志着 RDB 文件正文内容的结束
check_sum
是一个 8
字节长的无符号整数, 保存着一个校验和,以此来检查 RDB 文件是否出错或损坏
我并不想深入探究databases的组成。就是知道
- RDB 文件是一个经过压缩的二进制文件,由多个部分组成。
- 对于不同类型的键值对, RDB 文件会使用不同的方式来保存它们。
即可。
AOF
AOF持久化是通过保存服务器执行的命令来记录状态的。还原的时候再执行一遍即可。
功能的实现可以分为命令追加、文件写入、文件同步三个步骤。
当 AOF 持久化功能处于打开状态时, 服务器在执行完一个写命令之后, 会以协议格式将被执行的写命令追加到服务器状态的 aof_buf
缓冲区的末尾:
struct redisServer {// ...// AOF 缓冲区sds aof_buf;// ...
};
Redis 服务器进程就是一个事件循环
循环中的文件事件负责接收客户端的命令请求, 以及向客户端发送命令回复,
而时间事件则负责执行像 serverCron
函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令, 使得一些内容被追加到 aof_buf
缓冲区里面, 所以在服务器每次结束一个事件循环之前, 它都会调用 flushAppendOnlyFile
函数, 考虑是否需要将 aof_buf
缓冲区中的内容写入和保存到 AOF 文件里面, 这个过程可以用伪代码表示:
def eventLoop():while True:# 处理文件事件,接收命令请求以及发送命令回复# 处理命令请求时可能会有新内容被追加到 aof_buf 缓冲区中processFileEvents()# 处理时间事件processTimeEvents()# 考虑是否要将 aof_buf 中的内容写入和保存到 AOF 文件里面flushAppendOnlyFile()
flushAppendOnlyFile
函数的行为由服务器配置的 appendfsync
选项的值来决定
值为 always
时, 服务器在每个事件循环都要将 aof_buf
缓冲区中的所有内容写入到 AOF 文件并且同步 AOF 文件, 所以 always
的效率最慢的一个, 但从安全性来说, always
是最安全的, 因为即使出现故障停机, AOF 持久化也只会丢失一个事件循环中所产生的命令数据。
值为 everysec
时, 服务器在每个事件循环都要将 aof_buf
缓冲区中的所有内容写入到 AOF 文件, 每隔超过一秒就要在子线程中对 AOF 文件进行一次同步: 从效率上来讲, everysec
模式足够快, 并且就算出现故障停机, 数据库也只丢失一秒钟的命令数据。
值为 no
时, 服务器在每个事件循环都要将 aof_buf
缓冲区中的所有内容写入到 AOF 文件, 至于何时对 AOF 文件进行同步, 则由操作系统控制。
因为处于 no
模式下的 flushAppendOnlyFile
调用无须执行同步操作, 所以该模式下的 AOF 文件写入速度总是最快的, 不过因为这种模式会在系统缓存中积累一段时间的写入数据, 所以该模式的单次同步时长通常是三种模式中时间最长的: 从平摊操作的角度来看,no
模式和 everysec
模式的效率类似, 当出现故障停机时, 使用 no
模式的服务器将丢失上次同步 AOF 文件之后的所有写命令数据。
重写
AOF持久化是保存了一堆命令来恢复数据库,随着时间流逝,存的会越来越多,如果不加以控制,文件过大可能影响服务器甚至计算机。而且文件过大,恢复时需要时间也太长。
所以redis提供了重写功能,写出的新文件不会包含任何浪费时间的冗余命令。
接下来,我们就介绍重写的原理。
其实重写不会对现有的AOF文件进行读取分析等操作,而是通过当前服务器的状态来实现。
# 假设服务器对键list执行了以下命令s;
127.0.0.1:6379> RPUSH list "A" "B"
(integer) 2
127.0.0.1:6379> RPUSH list "C"
(integer) 3
127.0.0.1:6379> RPUSH list "D" "E"
(integer) 5
127.0.0.1:6379> LPOP list
"A"
127.0.0.1:6379> LPOP list
"B"
127.0.0.1:6379> RPUSH list "F" "G"
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1
1) "C"
2) "D"
3) "E"
4) "F"
5) "G"
127.0.0.1:6379>
当前列表键list在数据库中的值就为["C", "D", "E", "F", "G"]。要使用尽量少的命令来记录list键的状态,最简单的方式不是去读取和分析现有AOF文件的内容,,而是直接读取list键在数据库中的当前值,然后用一条RPUSH list "C" "D" "E" "F" "G"代替前面的6条命令。
- 伪代码表示如下
def AOF_REWRITE(tmp_tile_name):f = create(tmp_tile_name)# 遍历所有数据库for db in redisServer.db:# 如果数据库为空,那么跳过这个数据库if db.is_empty(): continue# 写入 SELECT 命令,用于切换数据库f.write_command("SELECT " + db.number)# 遍历所有键for key in db:# 如果键带有过期时间,并且已经过期,那么跳过这个键if key.have_expire_time() and key.is_expired(): continueif key.type == String:# 用 SET key value 命令来保存字符串键value = get_value_from_string(key)f.write_command("SET " + key + value)elif key.type == List:# 用 RPUSH key item1 item2 ... itemN 命令来保存列表键item1, item2, ..., itemN = get_item_from_list(key)f.write_command("RPUSH " + key + item1 + item2 + ... + itemN)elif key.type == Set:# 用 SADD key member1 member2 ... memberN 命令来保存集合键member1, member2, ..., memberN = get_member_from_set(key)f.write_command("SADD " + key + member1 + member2 + ... + memberN)elif key.type == Hash:# 用 HMSET key field1 value1 field2 value2 ... fieldN valueN 命令来保存哈希键field1, value1, field2, value2, ..., fieldN, valueN =\get_field_and_value_from_hash(key)f.write_command("HMSET " + key + field1 + value1 + field2 + value2 +\... + fieldN + valueN)elif key.type == SortedSet:# 用 ZADD key score1 member1 score2 member2 ... scoreN memberN# 命令来保存有序集键score1, member1, score2, member2, ..., scoreN, memberN = \get_score_and_member_from_sorted_set(key)f.write_command("ZADD " + key + score1 + member1 + score2 + member2 +\... + scoreN + memberN)else:raise_type_error()# 如果键带有过期时间,那么用 EXPIREAT key time 命令来保存键的过期时间if key.have_expire_time():f.write_command("EXPIREAT " + key + key.expire_time_in_unix_timestamp())# 关闭文件f.close()
AOF后台重写
aof_rewrite函数可以创建新的AOF文件,但是这个函数会进行大量的写入操作,所以调用这个函数的线程被长时间的阻塞,因为服务器使用单线程来处理命令请求;所以如果直接是服务器进程调用AOF_REWRITE函数的话,那么重写AOF期间,服务器将无法处理客户端发送来的命令请求;
Redis不希望AOF重写会造成服务器无法处理请求,所以将AOF重写程序放到子进程(后台)里执行。这样处理的好处是:
1)子进程进行AOF重写期间,主进程可以继续处理命令请求;
2)子进程带有主进程的数据副本,使用子进程而不是线程,可以避免在锁的情况下,保证数据的安全性。
还有一个问题,可能重写的时候又有新的命令过来,造成信息不对等,所以redis设置了一个缓冲区,重写期间把命令放到重写缓冲区。
总结
AOF重写的目的是为了解决AOF文件体积膨胀的问题,使用更小的体积来保存数据库状态,整个重写过程基本上不影响Redis主进程处理命令请求;
AOF重写其实是一个有歧义的名字,实际上重写工作是针对数据库的当前状态来进行的,重写过程中不会读写、也不适用原来的AOF文件;
AOF可以由用户手动触发,也可以由服务器自动触发。