MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Redis字典的持久化策略与实现

2023-08-146.9k 阅读

Redis字典概述

Redis是一个基于内存的高性能键值对存储数据库,其内部使用了多种数据结构来实现不同的数据类型,字典(dict)是其中非常关键的一种数据结构,用于实现Redis的数据库以及哈希(Hash)数据类型等。

Redis字典是一个哈希表结构,它由哈希表数组和链表组成,采用链地址法来解决哈希冲突。哈希表数组的每个元素是一个指向链表表头的指针,当不同的键经过哈希函数计算得到相同的哈希值时,这些键值对就会被存储在同一个链表中。

哈希表结构

Redis的哈希表结构定义在dict.h头文件中,其结构体定义如下:

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
  • table:是一个数组,数组元素是指向dictEntry结构体的指针,dictEntry用于存储键值对。
  • size:哈希表的大小,即table数组的长度。
  • sizemask:用于计算哈希值在table数组中的索引位置,其值为size - 1
  • used:哈希表中已使用的节点数量。

字典结构

字典结构体定义如下:

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;
  • type:指向一个dictType结构体的指针,dictType定义了针对不同数据类型的操作函数,如哈希函数、比较函数等。
  • privdata:私有数据指针,用于传递一些特定于应用的数据给dictType中的操作函数。
  • ht:包含两个哈希表,ht[0]用于正常存储数据,ht[1]在进行rehash操作时使用。
  • rehashidx:用于记录rehash的进度,如果值为-1,表示当前没有进行rehash操作。
  • iterators:当前正在运行的迭代器数量。

Redis持久化简介

Redis提供了两种主要的持久化机制:RDB(Redis Database)和AOF(Append - Only File),用于将内存中的数据保存到磁盘上,以便在Redis重启时能够恢复数据。

RDB持久化

RDB持久化是将Redis在某个时间点的内存数据以快照的形式保存到磁盘上。它会创建一个经过压缩的二进制文件,文件名通常为dump.rdb。RDB持久化的优点是:

  • 适合用于数据备份,因为生成的RDB文件紧凑,占用空间小,可以很方便地进行传输和恢复。
  • 恢复数据时速度快,因为可以直接将RDB文件读入内存。

然而,RDB也有其缺点:

  • 由于是定期生成快照,可能会丢失最近一次快照之后的数据修改,在故障恢复时可能会造成数据不一致。
  • 生成RDB文件时,Redis主线程会进行fork操作创建子进程,在大数据量情况下,fork操作可能会导致主线程阻塞。

AOF持久化

AOF持久化是将Redis执行的写命令以追加的方式保存到文件中,文件名通常为appendonly.aof。AOF的优点在于:

  • 数据完整性更高,因为它可以配置为每执行一条写命令就同步到磁盘,这样在故障恢复时可以最大限度地减少数据丢失。
  • 日志文件内容可读性强,方便进行故障排查和数据恢复。

AOF的缺点主要有:

  • AOF文件通常比RDB文件大,因为它记录的是命令而不是数据的最终状态。
  • 由于需要频繁进行文件写入操作,在高并发写入场景下,可能会对性能产生一定影响。

Redis字典在RDB中的持久化策略与实现

RDB持久化流程

  1. 触发机制:RDB持久化可以通过配置文件中的save参数设置定期触发,例如save 900 1表示在900秒内如果有1个键被修改,就触发RDB持久化;也可以通过SAVEBGSAVE命令手动触发。SAVE命令会阻塞主线程进行RDB文件生成,而BGSAVE命令会fork一个子进程来进行RDB文件生成,主线程继续处理客户端请求。
  2. 数据序列化:当触发RDB持久化时,Redis会遍历数据库中的所有字典,将字典中的键值对进行序列化。对于哈希表结构的字典,会逐个遍历哈希表数组中的链表,将每个dictEntry中的键值对按照一定的格式进行编码。
  3. 文件生成:序列化后的数据会被写入到RDB文件中,RDB文件采用特定的二进制格式,包含了版本信息、数据库数量、每个数据库中的键值对等内容。在写入过程中,会对数据进行压缩以减少文件大小。

字典键值对的序列化

Redis在RDB持久化中对字典键值对的序列化过程如下:

  1. 键的序列化:首先会根据键的类型,调用相应的编码函数将键转换为二进制格式。例如,对于字符串类型的键,会先记录字符串的长度,然后记录字符串的内容。
  2. 值的序列化:值的序列化方式取决于值的类型。对于简单的字符串类型值,同样记录长度和内容;对于复杂的数据类型,如哈希、列表等,会按照各自的数据结构特点进行递归序列化。
  3. 写入文件:序列化后的键值对会按照一定的顺序写入RDB文件,在读取RDB文件恢复数据时,会按照相同的顺序反序列化并重建字典。

代码示例(简化的RDB持久化字典部分代码)

// 假设已有函数获取Redis字典
dict* getRedisDict(); 

// 简化的键值对序列化函数
void serializeKeyValuePair(dictEntry *entry, FILE *rdbFile) {
    robj *key = dictGetKey(entry);
    robj *val = dictGetVal(entry);
    // 序列化键
    int keyLen = sdslen(key->ptr);
    fwrite(&keyLen, sizeof(int), 1, rdbFile);
    fwrite(key->ptr, keyLen, 1, rdbFile);
    // 序列化值
    // 假设值为字符串类型,这里简化处理
    int valLen = sdslen(val->ptr);
    fwrite(&valLen, sizeof(int), 1, rdbFile);
    fwrite(val->ptr, valLen, 1, rdbFile);
}

// 简化的RDB持久化字典函数
void rdbPersistDict(dict *dict, FILE *rdbFile) {
    dictht *ht = &dict->ht[0];
    for (unsigned long i = 0; i < ht->size; i++) {
        dictEntry *entry = ht->table[i];
        while (entry) {
            serializeKeyValuePair(entry, rdbFile);
            entry = entry->next;
        }
    }
}

在实际的Redis源码中,RDB持久化涉及到更复杂的逻辑,包括处理不同数据类型的编码、版本兼容性等,但上述代码展示了基本的字典键值对序列化和持久化思路。

Redis字典在AOF中的持久化策略与实现

AOF持久化流程

  1. 命令追加:当Redis执行写命令时,会将该命令追加到AOF缓冲区中。对于涉及字典操作的命令,如HSET(用于在哈希字典中设置键值对)、HDEL(用于删除哈希字典中的键值对)等,会将这些命令以文本形式记录下来。
  2. 文件同步:AOF缓冲区中的命令会根据配置的同步策略定期或实时地写入到AOF文件中。可以通过appendfsync参数配置同步策略,如always表示每执行一条写命令就同步到磁盘,everysec表示每秒同步一次,no表示由操作系统决定何时同步。
  3. 重写机制:随着Redis运行时间的增加,AOF文件会不断增大,为了避免文件过大影响性能和恢复时间,Redis提供了AOF重写机制。AOF重写会根据当前内存中的数据生成一个优化后的AOF文件,去除冗余的命令,只保留能恢复数据的最小命令集。

字典相关命令的记录

HSET命令为例,当执行HSET key field value命令时,Redis会将该命令以文本形式追加到AOF缓冲区,格式类似*4\r\n$4\r\nHSET\r\n$3\r\nkey\r\n$5\r\nfield\r\n$5\r\nvalue\r\n。这里采用了Redis协议(RESP)格式,*4表示后面有4个参数,$4表示第一个参数HSET的长度为4,以此类推。

当执行HDEL key field1 field2命令时,记录格式为*4\r\n$4\r\nHDEL\r\n$3\r\nkey\r\n$6\r\nfield1\r\n$6\r\nfield2\r\n

AOF重写中的字典处理

在AOF重写过程中,Redis会遍历内存中的字典数据结构,根据字典的当前状态生成最精简的命令集。例如,对于一个哈希字典,如果在重写时字典中有多个键值对,重写过程会生成一系列HSET命令来重建这个哈希字典,而不是记录之前所有的修改命令。

代码示例(简化的AOF记录字典操作命令代码)

// 假设已有函数获取当前命令参数
void* getCommandArgs(); 
int getCommandArgCount(); 

// 简化的AOF记录命令函数
void aofLogCommand(FILE *aofFile) {
    int argCount = getCommandArgCount();
    // 先写入参数数量
    char buf[16];
    snprintf(buf, sizeof(buf), "*%d\r\n", argCount);
    fwrite(buf, strlen(buf), 1, aofFile);
    void **args = getCommandArgs();
    for (int i = 0; i < argCount; i++) {
        char *arg = (char *)args[i];
        int argLen = strlen(arg);
        // 写入参数长度
        snprintf(buf, sizeof(buf), "$%d\r\n", argLen);
        fwrite(buf, strlen(buf), 1, aofFile);
        // 写入参数内容
        fwrite(arg, argLen, 1, aofFile);
        fwrite("\r\n", 2, 1, aofFile);
    }
}

上述代码展示了如何将命令以RESP格式记录到AOF文件中,实际的Redis AOF实现中还包括对不同命令的特殊处理、缓冲区管理、重写逻辑等更复杂的内容。

混合持久化

为了结合RDB和AOF的优点,Redis从4.0版本开始引入了混合持久化。混合持久化在进行持久化时,会先将内存中的数据以RDB格式写入AOF文件开头部分,然后再将后续的写命令以AOF格式追加到文件中。

混合持久化流程

  1. RDB部分写入:在触发持久化时,首先按照RDB的方式将当前内存中的数据进行快照,并将其写入AOF文件的开头部分。这部分数据可以在Redis重启时快速加载到内存中,恢复大部分数据状态。
  2. AOF部分追加:在RDB数据写入完成后,继续按照AOF的方式将持久化开始之后的写命令追加到AOF文件中。这样可以保证从持久化开始到结束期间的数据修改也能被记录下来,从而实现完整的数据恢复。

混合持久化的优势

  • 快速恢复:由于开头部分是RDB格式的数据,在重启时可以快速加载到内存中,相比纯AOF方式,大大缩短了恢复时间。
  • 数据完整性:后续追加的AOF部分记录了持久化过程中的所有数据修改,保证了数据的完整性,减少了数据丢失的风险。

混合持久化中的字典处理

对于字典数据,在混合持久化的RDB部分,同样按照RDB的字典持久化方式进行处理,将字典中的键值对序列化后写入AOF文件。在AOF追加部分,对于字典相关的写命令,如HSETHDEL等,按照AOF的记录方式追加到文件中。

总结

Redis字典的持久化策略在RDB、AOF以及混合持久化中各有特点。RDB适合快速备份和恢复,AOF保证数据完整性,混合持久化则结合了两者的优势。深入理解这些持久化策略与实现,对于优化Redis性能、确保数据可靠性以及进行故障恢复都具有重要意义。在实际应用中,需要根据具体的业务需求和数据特点,合理选择和配置持久化方式,以达到最佳的效果。无论是RDB中字典键值对的序列化,还是AOF中命令的记录与重写,都体现了Redis在持久化设计上的精心考量,使得Redis能够在高性能和数据可靠性之间找到平衡。