Redis持久化机制

Redis是基于内存的,一旦程序退出,存储在内存的数据都会被清空,为了解决程序结束导致数据丢失的问题,Redis为我们提供了两种数据持久化的方式

RDB

使用RDB会将Redis在内存中的数据库状态保存到磁盘中,可以被手动执行,也可以自动执行,它只能将某个时间点的数据库状态快照下来生成*.rdb文件

RDB文件的创建

生成RDB文件,可以使用SAVEBGSAVE命令

SAVE

save命令会阻塞服务器进程,直到生成RDB文件完毕

BGSAVE

BGSAVE会fork出一个子进程来负责创建RDB文件,父进程仍然可以继续处理服务器请求

两个命令会分别以不同的方式调用rdbSave()函数来执行RDB
  • save

    // 伪代码
    void save() {
        rdbSave();
    }
    
  • bgsave

    // 伪代码
    void bgSave() {
        pid = fork();
    
        if (pid == 0) {
            // 子进程负责执行rdbSave()
            rdbSave();
            // 完成rdbSave()后向父进程发送信号
            signalToParent();
        } else if (pid > 0) {
            // 执行请求,并等待子进程的信号
            handleRequestAndWaitSignal();
        } else {
            handleForkError();
        }
    }
    

在执行BGSAVE命令的时候,服务器主进程还能够接收请求和处理请求,但是在执行命令期间

  • 客户端发送的SAVE命令会被拒绝
  • BGSAVE命令也会被拒绝
  • 如果执行BGSAVE时,BGREWRITEAOF已经在执行了,那么BGSAVE会被拒绝;如果BGSAVE先执行,那么BGREWRITEAOF会等待BGSAVE执行完毕后再执行

RDB文件的载入

在服务器启动时,如果检测到了RDB文件,那么服务器就会自动执行RDB载入过程,载入过程中,服务器会被阻塞直到载入完成

如果服务器同时开启了RDB和AOF,那么服务器会优先使用AOF文件来还原数据库状态,因为AOF文件的数据丢失的情况比较少

RDB文件使用rdbLoad()函数完成
  • 服务器启动,执行载入程序
  • 检测是否开启AOF
    • 开启,载入AOF文件
    • 关闭,载入RDB文件

RDB文件的结构

REDIS | db_version | databases | EOF | check_sum

  • REDIS,代表常量字符,'R','E','D','I','S'
  • db_version,4字节,记录了RDB文件的版本号
  • databases,包含0个或n个数据库,如果数据库状态为空,那么它为0
  • EOF,1个字节,标志着RDB文件的结束
  • check_sum,文件校验和,在载入的时候会通过该值确定RDB文件是否出错或者损坏

例:

​ REDIS | 0006 | EOF | 6265312314761917404

​ REDIS | 0006 | database 0 | database 1 | database 2 | EOF | 6265312314761917404

database 结构

​ SELECTDB(1 byte) | db_number | key_value_pairs

不带过期键的key_value_pairs结构

​ TYPE | key | value

例:

​ REDIS_RDB_TYPE_STRING | key | value

带有过期键的key_value_pairs结构

​ EXPIRETIME_MS | ms | TYPE | key | value

例:

​ EXPIRETIME_MS | 1388556000000 | REDIS_RDB_TYPE_SET | key | value

AOF

append only file,也是Redis提供的一种持久化方式,它通过保存Redis服务器所执行的写命令来记录数据库状态

AOF持久化功能的实现可以分为三个步骤:

  • 命令追加(append)
  • 文件写入
  • 文件同步(fsync)
命令追加
  • 当AOF功能处于打开状态时,服务器每执行一个写命令后会以协议格式将被执行的写命令追加到服务器中的aof_buf缓冲区的末尾
文件写入
  • Redis服务器就是一个事件循环,每次服务器结束一个事件循环后就会调用flushAppendOnlyFile方法,考虑是否需要将aof_buf缓冲区的内容写入和保存到AOF文件中
  • flushAppendOnlyFile()方法的行为由服务器配置的appendfsync选项的值来决定
    • always,总是将aof_buf中的内容同步并写入到AOF文件中
    • everysec(默认),将aof_buf中的内容写入到AOF文件中,如果距离上次AOF文件同步的时间超过一秒钟,那么再次对AOF文件进行同步,同步操作由统一线程专门负责执行
    • no,将aof_buf中的内容写入到AOF文件中,但何时同步由系统决定
// 伪代码
void eventLop() {
    while (true) {
        // 处理文件事件,此时可能会有新的内容被追加到aof_buf缓冲区中
        processFileEvents();
        // 处理时间事件
        processTimeEvents();
        // 考虑是否要将aof_buf缓冲区的内容写入和保存到AOF文件中
        flushAppendOnlyFile();
    }
}

AOF文件的载入与数据还原

  • 服务器启动载入程序,创建fake client,这个client不带网络连接,因为载入AOF不需要网络操作
  • 循环从AOF文件中分析读取写命令,并用fake client执行命令,直到AOF文件被处理完毕

AOF重写

  • 使用bgrewriteaof命令来触发AOF重写。

AOF文件的体积会随着服务器运行的时间越来越大,如果体积过大可能对整个服务器甚至计算机产生影响,并且用于还原的时间也越长,为了解决这个问题,Redis提供了AOF文件重写的功能,通过创建新的AOF文件来替换原来的AOF文件,新的AOF文件所保存的数据库状态相同,但是不会包含浪费空间的冗余命令

如何重写

AOF文件的重写不需要对现有的AOF文件进行分析、读取或者写入操作,它是通过读取服务器当前的数据库状态来实现的

例如,我为list键分别push了6个值,我们执行了6条命令并append到AOF文件中,在重写的时候,服务器会用尽量少的命令来记录list键的状态,因此它会直接从数据库读取list键的值,然后用一条命令来代替保存在老AOF文件中的6条命令

// 伪代码
void aofRewrite(newAOFFile) {
    // 创建新的AOF文件
    File aof = new File(newAOFFile);
    
    // 遍历数据库
    for (redisDb db : redisServer.db) {
        if (db.isEmpty()) {
            // 忽略空数据库
            continue;
        }
        
        // 选定数据库号码
        aof.writeCommand("select" + db.id);
        for (String key : db.dict) {
            // 忽略已经过期的键
            if (key.isExpired()) {
                continue;
            }
            reWrite(key);
            // 如果键带有过期时间,那么过期时间也要被重写
            if (key.hasExpireTime()) {
                reWriteExpireKey(key);
            }
        }
    }
    
    void reWrite(String key) { // ... }
    void reWrite(List key) { // ... }
    void reWrite(Set key) { // ... }
    void reWrite(ZSet key) { // ... }    
    void reWrite(Hash key) { // ... }
    <K> void reWriteExpireKey(K key) { //... }
}
AOF后台重写

重写AOF,会阻塞进程,因此Redis在AOF重写时也会fork()一个子进程用于重写AOF

  • 那么父进程可以继续处理请求而不必阻塞,影响性能
  • 子进程拥有的是父进程数据的副本
  • 不用线程避免了使用锁的情况下保证数据的安全性

但是在AOF重写期间,父进程也在接收和处理请求,那么当前服务器数据库的状态会和重写后的AOF文件保存的状态不一致

因此Redis设置了一个AOF重写缓冲区,这个缓冲区会在服务器创建子进程之后开始使用,当Redis主进程执行一个写命令之后会同时将这个写命令发送给AOF重写缓冲区

当子进程完成重写后会向父进程发送一个信号,父进程接收到信号之后会调用信号处理函数,然后

  • 将AOF重写缓冲区中的内容写入到新的AOF文件中
  • 对新的AOF文件进行改名,原子的覆盖当前的AOF文件,会阻塞进程