Redis是内存数据库,如果不想办法保存到磁盘里,那么一旦服务退出,保存的数据也会丢失。Redis持久化可分为RDB和AOF。RDB中是一个经压缩的二进制文件,存的是格式化后的数据,AOF保存的是执行的命令。因为AOF文件更新的频率比RDB高,所以只有Redis在AOF持久化功能处于关闭状态时,才会使用RDB模式。

RDB

SAVEBGSAVE都可让Redis生成RDB文件。SAVE会阻塞服务,直到RDB文件生成为止。BGSAVE会派生出一个子进程,父进程继续执行命令。

RDB文件的载入不用手工进行,当Redis服务启动时会自动检测RDB文件是否存在。Redis允许配置save选项来定时执行BGSAVE。

1
2
3
# 900秒内对数据库至少执行1次修改
# 则执行BGSAVE,可配置多个
save 900 1

在Redis服务器启动时,会去检查用户有没有配置save选项,如果没有会用如下默认save选项,此选项可在redis.conf文件中配置。

1
2
3
save 900 1
save 300 10
save 60 10000

接着服务器会根据选项设置服务器状态redisServer结构的saveparams(数组)属性:

1
2
3
4
5
6
7
8
9
10
11
struct redisServer {
// ...
struct saveparam *saveparams;
// ...
}
struct saveparam {
// 秒数
time_t seconds;
// 修改次数
int change;
}

除此之外Redis还维持了dirtylastsave属性。dirty记录上次SAVE或BGSAVE之后服务器状态的修改次数,lastsave是个时间戳,记录上次SAVE/BGSAVE的时间。Redis周期性事件每个100毫秒就会执行一次,其中一项任务就是查看save选项是否满足条件,如果满足就执行BGSAVE命令。

RDB的结构类似如下(还没有展开):

1
| REDIS | db_version | database | EOF | check_sum |

REDIS表示“REDIS”这5个字符,db_version代表RDB文件版本号,database代表任意多个数据库以及数据库中的键值对数据,EOF代表文件结束,check_sum是校验符。

Copy On Write(COW)

调用fork()函数会产生一个子进程,子进程和父进程共享代码段和数据段。子进程做持久化时,不会改变内存数据,父进程会继续处理客户端命令,对内存进行修改,这个时候就会使用操作系统的COW机制来进行数据页分离。当父进程对其中一个页进行修改时,会将被共享的页复制一份出来,然后对这个复制的页进行修改,子进程相应的页没有改变。

AOF

AOF通过记录Redis所执行的命令来保存数据库的状态,可在配置文件中的appendonly yes打开。例如数据库执行以下写命令:

1
2
3
4
redis> SET msg "hello"
OK
redis> RPUSH numbers 1 2 3
(integer) 3

RDB持久化的做法是将msg和numbers两个键值对保存在文件中,而AOF持久化保存数据库状态的方法是将执行的SET、RPUSH命令保存到AOF文件中。

AOF持久化分为追加(append)、文件写入、文件同步三个步骤,同步指将缓冲区数据刷入磁盘。当打开AOF功能后,服务区执行完一个写命令,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾,aof_buf类型为sds(简单动态字符串)。当客户端发送SET KEY VALUE到服务器时,服务器执行完这个SET命令后会将这个SET命令追加到aof_buf中。

Redis服务器运行就是一个事件循环,在这个循环中处理命令请求、向客户端回复这种文件任务和一些处理定时任务,在每个事循环之前,都会调用flushAppendOnlyFile检查是否将aof_buf缓冲区的内容写入AOF文件中,这个行为可以在配置文件的appendfsync指定:

  • always:将aof_buf中的内容全部写入并同步到AOF中
  • everysec:将aof_buf全部写入到AOF,如果上次同步AOF的事件距离现在超过1秒,那么再次对AOF文件进行同步,并有一个线程单独负责
  • no:将aof_buf全部写入到AOF,但并不对AOF进行同步,何时操作有操作系统决定

当数据写入文件时,操作系统会将其写入到一个缓冲区中,等到缓冲区满、或超时后,再将数据写入到磁盘中。这种做法带来了安全问题,如果计算机发生停机,那么保存再缓冲区的数据将会丢失。

  • 当appendfsync为always时,每次事件循环后都要将aof_buf的内容写入AOF中,并且同步AOF,所以always是效率最慢的,同时always也是最安全的,即使机器发生故障,也只会丢失一个事件循环中所产生的命令数据

  • 当appendfsync的值为everysec时,服务器在每次事件循环中都要将aof_buf的内容写入AOF中,并且每个一秒中就要在子线程中对AOF进行一次同步。当出现故障时,只会丢失一秒钟的命令数据

  • 当appendfsync的值为no时,每次事件循环后,都要将aof_buf中的内容写入AOF中,至于何时同步AOF,则由操作系统控制。这种模式下无需执行同步操作,所以写入AOF的速度最快

AOF重写

AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着AOF中内容越来越多,文件体积会越来越大。例如:

1
2
3
4
5
6
7
8
> RPUSH list A # ["A"]
(integer) 1
> RPUSH list B # ["A", "B"]
(integer) 2
> RPUSH list C # ["A", "B", "C"]
(integer) 3
> LPOP list # ["B", "C"]
(integer) 2

为了保存list的状态,就要记录4条指令,实际的情况会更加复杂。Redis提供了AOF重写功能(rewrite),来减小AOF文件膨胀问题。通过该功能,Redis服务器会创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的状态相同,但新AOF文件不会保存冗余的命令。重写功能实际上并不会对现有AOF文件进行任何操作,这个功能是通过读取服务器当前数据库状态来实现的。上面的4条命令会被重写为:

1
> RPUSH list B C

这样就只存储了1条指令。其他所有类型的键可以用同样的方法来减少AOF文件中命令的数量。当重写过程遇到列表、哈希表、集合、有序集合这四种可能带有多个元素的键时,会检查所包含键的个数,如果超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量值(64),将采用多条命令来记录,这样可以防止客户端缓冲区溢出。

AOF后台重写程序可以很好地完成创建一个新AOF文件的任务,因为这个任务会进行大量的写操作,所以调用这个过程的线程将长期阻塞。因为Redis是单线程的,所以在AOF重写期间,服务器将无法处理客户端的命令请求,所以Redis在子进程中执行AOF重写,这样有两个好处:

  • 重写期间可以继续处理命令
  • 子进程带有服务器数据副本,使用子进程而不是子线程,可以在不使用锁的情况保证数据安全

不过在子进程进行AOF重写期间,服务器会继续处理命令,从而对数据库状态进行修改,造成重写后AOF与数据库状态不一致。为了解决这一问题,Redis设置了一个重写缓冲区,它会在重写之后开始使用,当Redis执行完一个写命令后,会同时发送给AOF缓冲区和AOF重写缓冲区。AOF缓冲区会被定期写入和同步到AOF文件,对现有的AOF文件处理工作如常进行。从创建子进程开始,服务器执行的写命令都会被记录到AOF重写缓冲区中。

当子进程完成重写后,会向父进程发送信号,父进程会进行如下过程:

  • 将AOF重写缓冲区中的内容写入新AOF文件中
  • 替换旧的AOF文件

这个过程结束后,父进程就可以像往常一样接受命令了。在整个AOF重写过程中,只有信号处理函数会阻塞服务器,对服务器的影响降到了最低。