状态

Redis是一个内存数据库,运行在内存中,其中可能包含多个数据库以及数据库中包含多个键值对,将Redis中这些非空的数据库和其中的键值对称为状态

Redis的数据都存放在内存中,如果不将内存中的数据库状态保存到硬盘上,那么Redis进程结束的时候,本次运行期间数据库中所有的内容都不回被保存,下次启动Redis的时候又只是一个空数据库。

为了解决这个问题,Redis使用了持久化功能,这个功能可以将Redis中的数据保存到硬盘上进行持久存储,防止数据的意外丢失。

RDB持久化

RDB持久化可以使用save命令来进行手动存储,也可以配置服务器自动定期执行存储。

RDB读取Redis的状态来得到数据库当前存储的信息,然后将其压缩为一个RDB文件;在需要恢复Redis数据库数据的时候,在从RDB文件中读取保存的信息从而恢复数据。

持久化 - 图1

创建RDB文件

有两个命令可以来创建RDB文件,一个是SAVE、一个是BGSAVE

  • Save命令会阻塞Redis进程,所以从Save执行直到结束,用户所有的命令都不能执行。
  1. 127.0.0.1:6379> save
  2. OK
  • BGSave命令会**fork**一个子进程,然后子进程来完成RDB文件的创建,服务器进程继续执行命令请求。
  1. 127.0.0.1:6379> bgsave
  2. Background saving started

实际上,创建RDB的工作是由rdbSave()函数来执行的,SAVEBGSAVE命令只是在不同的时候调用了这个函数。
伪代码如下:

  1. function SAVE(){
  2. rdbSave(); // 直接进行调用,导致阻塞
  3. }
  4. function BGSAVE(){
  5. // 创建子进程
  6. pid = fork();
  7. if(pid==0) {
  8. // pid=0代表使用子进程来执行
  9. // 所以主进程不阻塞
  10. rdbSave();
  11. } else if (pid > 0) {
  12. # 父进程继续执行用户发送的命令
  13. ....
  14. }
  15. }

自动保存RDB文件

由于BGSAVE在执行期间不阻塞用户的请求,所以可以在conf文件中设置相应的命令来使Redis自动的保存当前数据库的状态,生成RDB文件。

conf中默认的运行BGSAVE的参数如下:

  1. save 900 1 # 代表如果900秒以内,数据库有最少一个修改,就执行BGSAVE
  2. save 300 10 # 如果300秒以内,数据库有最少十个修改,就执行BGSAVE
  3. save 60 10000 # 如果60秒以内,数据库有最少10000个修改,就执行BGSAVE

可以在conf文件中配置多个save,满足其中一个则自动执行BGSAVE

设置保存条件

服务器启动期间,读取conf文件中的save配置,生成一个保存所有save条件的数组,

  1. struct saveparam *saveparams;
  2. struct saveparam{
  3. // save配置的秒数
  4. time_t seconds;
  5. // save配置的改变的次数
  6. int changes;
  7. }

假如配置的是:

  1. save 900 1 # 代表如果900秒以内,数据库有最少一个修改,就执行BGSAVE
  2. save 300 10 # 如果300秒以内,数据库有最少十个修改,就执行BGSAVE
  3. save 60 10000 # 如果60秒以内,数据库有最少10000个修改,就执行BGSAVE

那么最终会生成:
持久化 - 图2

dirty计数器+lastsave属性

dirty计数器记录了自上次成功执行SAVE或者BGSAVE以后,服务器对数据库状态修改的次数,包括添加、删除、更新等操作。

lastsave则记录了上一次执行保存操作时候的UNIX时间戳。

检查是否满足执行BGSAVE的条件

Redis有一个周期性函数serverCron默认每100ms执行一次,它的其中一个工作就是检查conf文件中save选项的条件是不是已经满足,如果满足,执行BGSAVE

  1. function serverCron(){
  2. .....
  3. // 遍历执行检查操作
  4. for(saveparam param:server.saveparams){
  5. int diff = now() - lastsave();// 距离上次更新的间隔时间
  6. if(diff >= param.seconds && dirty >= param.changes) {
  7. BGSAVE();
  8. }
  9. }
  10. ....
  11. }

程序遍历所有的save参数,如果有一个满足,就执行BGSAVE操作。

RDB总结

  • RDB文件可以使服务器恢复Redis数据库的所有键值对数据。
  • SAVE命令执行保存操作会阻塞服务器。
  • BGSAVE命令在后台执行,不会阻塞服务器。
  • conf中设置save参数来配置BGSAVE的执行条件。

优势

整个redis中只有这一个备份文件,不用经常进行备份。

适合大规模的数据恢复。性能最大化。

通过fork子进程来持久化,同时主进程又能继续处理客户端的请求。

相较于AOF机制,如果数据集很大,启动时数据恢复效率更高更快。

劣势

如果服务器突然宕机,还未来得及持久化的数据将会丢失。

如果对数据完整性要求较高,不建议采用这种方式。

由于是fork了一个与当前进程一样的进程,包含当前进程的所有数据,所以,内存中的数据增加了一倍,性能会有所影响。

AOF

RDB保存的是数据库的状态,而AOF保存的是Redis执行的所有写操作(读操作对数据库状态没有影响,不需要进行记录)。

持久化 - 图3

当服务器执行:

  1. 127.0.0.1:6379> set msg vms
  2. OK
  3. 127.0.0.1:6379> sadd schools qinghua beida ustc
  4. (integer) 3
  5. 127.0.0.1:6379> rpush numbers 1 2 3
  6. (integer) 3

AOF文件中保存的是:

  1. *2
  2. $6
  3. SELECT
  4. $1
  5. 0
  6. *3
  7. $3
  8. set
  9. $3
  10. msg
  11. $3
  12. vms
  13. *5
  14. $4
  15. sadd
  16. $7
  17. schools
  18. $7
  19. qinghua
  20. $5
  21. beida
  22. $4
  23. ustc
  24. *5
  25. $5
  26. rpush
  27. $7
  28. numbers
  29. $1
  30. 1
  31. $1
  32. 2
  33. $1
  34. 3

这些命令都是刚才执行过的写命令,当Redis服务器再次启动的时候,就可以从AOF文件中复原数据库的状态。

AOF追加实现方式

  1. 命令追加
    当AOF命令处于打开的时候,服务器执行完一个写命令后,会将执行的写命令按照所规定的格式加入到AOF_BUF缓冲区的末尾。
  2. 写入与同步
    Redis服务器就是一个事件的循环,其中
  • 文件事件:接受客户端的命令请求
  • 时间事件:执行需要定时执行的命令

每次服务器结束一个文件事件的时候,由于可能执行写命令,这会让一些内容被追加到AOF_BUF缓冲区中(设置缓冲区的目的是为了提高文件的写入效率,将需要写入的数据暂存到内存缓冲区,缓冲区满或者超过一定时间时候才将数据写入硬盘),所以每次在文件事件结束的时候,都会调用flushAppendOnlyFile函数,来决定时都需要将AOF_BUF缓冲区的内容加入到AOF文件中。
伪代码:

  1. function eventLoop(){
  2. while(true){
  3. // 处理文件事件
  4. processFileEvents();
  5. // 处理时间事件
  6. processTimeEvents();
  7. // 决定是否将aof缓冲区内容写入aof文件
  8. flushAppendOnlyFile();
  9. }
  10. }

flushAppendOnlyFile根据conf的配置来决定何时来执行AOF。

  1. # appendfsync always # AOF_BUF的所有内容都加入AOF文件
  2. appendfsync everysec # 每一秒钟将AOF_BUF的文件加入AOF文件
  3. # appendfsync no # 何时加入来由操作系统决定
  • appendfsync always如果突然断电,最多会丢失一次事件循环已经执行的命令,因为每次执行完毕都会写入到AOF,但是因为每次都写入AOF文件,所以效率稍低。
  • appendfsync everysec最多丢失一秒内执行的命令
  • appendfsync no丢失的命令数量=上次写入到AOF以后执行命令的次数。

数据还原

Redis读取AOF文件的步骤如下:

  1. 创建一个伪客户端,因为Redis命令只能在客户端上下文中执行,而载入AOF文件中的命令来源于AOF而不是来自客户端,所以创建一个伪客户端来执行AOF中的命令。
  2. 从AOF读取一条写入命令
  3. 伪客户端执行这个命令
  4. 重复2-3直到AOF文件中的命令执行完毕。

持久化 - 图4

AOF重写

如果AOF文件体积过大,那么通常会影响服务器性能,可以通过一些操作来缓解这种情况。
例如如下命令:

  1. 127.0.0.1:6379> RPUSH list A B # [A,B]
  2. (integer) 2
  3. 127.0.0.1:6379> RPUSH list C # [A,B,C]
  4. (integer) 3
  5. 127.0.0.1:6379> RPUSH list D E # [A,B,C,D,E]
  6. (integer) 5
  7. 127.0.0.1:6379> LPOP list # [B,C,D,E]
  8. "A"
  9. 127.0.0.1:6379> LPOP list # [C,D,E]
  10. "B"
  11. 127.0.0.1:6379> RPUSH list F G # [C,D,E,F,G]
  12. (integer) 5

其实这些都可以用一条命令

  1. RPUSH list C,D,E,F,G

AOF实现这个功能的操作就是重写。

重写的实现

AOF重写并不需要对现有的AOF文件进行读取分析,而是直接读取服务器的状态来实现
当执行这些指令来以后:

  1. 127.0.0.1:6379> RPUSH list A B # [A,B]
  2. (integer) 2
  3. 127.0.0.1:6379> RPUSH list C # [A,B,C]
  4. (integer) 3
  5. 127.0.0.1:6379> RPUSH list D E # [A,B,C,D,E]
  6. (integer) 5
  7. 127.0.0.1:6379> LPOP list # [B,C,D,E]
  8. "A"
  9. 127.0.0.1:6379> LPOP list # [C,D,E]
  10. "B"
  11. 127.0.0.1:6379> RPUSH list F G # [C,D,E,F,G]
  12. (integer) 5

如果想要实现AOF文件体积的最小化,那么就可以直接从数据库读取list键对应的值,可以得到:C,D,E,F,G

所以AOF重写可以概括为:

  1. 从数据库读取键现在的值
  2. 用一条命令(例如RPUSH list C,D,E,F,G)去记录这个键值对来代替之前的多条的命令。

整个过程:

  1. function aof_rewrite(newfilename){
  2. f = create_file(newfilename); // 新的aof文件
  3. for(Database db: server.Databases){
  4. if(db.empty) continue; // 当前数据库空,跳过
  5. for(Key k:keys){ // 遍历所有的key
  6. if key.is_expired() continue;
  7. f.write(k); // 写入key以及key对应的数据
  8. }
  9. }
  10. f.close();
  11. }
  • AOF重写是通过子进程实现的
    因为使用的是子进程,那么子进程对AOF重写的时候,服务器可能处理新的数据,改变AOF开始重写时刻以后的数据库状态,那么可能会导致当前数据库状态和重写后AOF记录的服务器状态不一致。
    为了解决这个问题,Redis设置了一个AOF重写缓冲区,这个缓冲区在子进程之后开始使用,Redis执行完一个写命令以后,同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区,这样可以保障:从子进程创建开始,所有的写命令都会被记录到AOF重写缓冲区。

当子进程完成后,给父进程发送信号,父进程调用信号处理函数,并执行:

  1. 将AOF重写缓冲区的内容写到写的AOF中,保证数据的一致性。
  2. 对新的AOF文件换名,原子性的覆盖现有的AOF文件,完成新旧AOF文件的替换。

优劣势

优势

更高的数据安全性和完整性。

默认情况下每秒同步,最多丢失一秒的数据。

后台异步的,效率非常高。

提供Redis-check-aof --fix机制,确保数据正确性。

劣势

AOF文件相较于RDB文件大得多,数据恢复效率低。

AOF虽然是后台异步fsync追加日志文件,无论是每秒同步还是每修改同步,都是消耗一部分性能。

如果同时开启两种持久化方式,redis重启时会采用AOF文件来恢复数据。因为AOF文件保存的数据比RDB要完整,RDB丢失数据的风险要大一些。

参考

  1. 《Redis设计与实现》
  2. https://juejin.cn/post/6847902220726304781#heading-4