单机数据库的实现(上)

数据库

文章目录

  1. 服务器中的数据库

概述

Redis 服务器实例可以包含多个数据库,默认情况下有 16 个数据库(编号从 0 到 15),用户可以通过配置文件中的 databases 参数修改数据库数量。

实现

Redis 的多个数据库实际上是以数组形式存在的,每个数据库在服务器启动时创建,并且在 Redis 服务器中以 redisDb 结构表示:

typedef struct redisDb {
    dict *dict;                 // 数据库键空间,保存所有键值对
    dict *expires;              // 过期时间字典,保存所有键的过期时间
    dict *blocking_keys;        // 阻塞操作相关
    dict *ready_keys;           // 阻塞操作相关
    dict *watched_keys;         // 事务相关
    int id;                     // 数据库编号
    long long avg_ttl;          // 平均TTL(过期时间)
} redisDb;

每个 redisDb 实例表示一个数据库,其中 dict 是一个字典,存储了所有的键值对。

数据结构图

以下是 Redis 数据库的结构图:

在这里插入图片描述

其他说明

  • 每个数据库的键空间和过期时间字典都是独立的,操作不会相互影响。
  • Redis 命令默认作用于当前选中的数据库,切换数据库使用 SELECT 命令。
  1. 切换数据库

概述

Redis 默认从 0 号数据库开始工作,可以通过 SELECT 命令切换到其他数据库。数据库切换只会影响当前连接,其他连接不会受影响。

实现

SELECT 命令用于切换数据库,接受一个参数,即数据库编号。命令的处理函数如下:

void selectCommand(client *c) {
    long id;
    if (getLongFromObjectOrReply(c,c->argv[1],&id,NULL) != C_OK)
        return;
    if (selectDb(c,id) == C_ERR) {
        addReplyError(c,"DB index is out of range");
    } else {
        addReply(c,shared.ok);
    }
}

selectDb 函数用于实际切换数据库:

int selectDb(client *c, int id) {
    if (id < 0 || id >= server.dbnum)
        return C_ERR;
    c->db = &server.db[id];
    return C_OK;
}
  • client 结构体中包含 db 指针,指向当前数据库。
  • 切换数据库时,db 指针会更新为目标数据库的地址。
数据结构图

以下是切换数据库时的结构图:

在这里插入图片描述

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其他说明

  • 数据库切换不影响其他客户端的数据库选择,每个客户端都有自己的当前数据库。
  • 使用 INFO keyspace 命令可以查看各个数据库的键数量和过期信息。
  1. 数据库键空间

概述

Redis 的键空间(key space)是数据库的核心部分,存储了所有的键值对。每个数据库的键空间由一个字典(dict)实现,提供高效的增删改查操作。

具体操作
添加新键

在 Redis 中添加新键通常通过 SETHSET 等命令完成。如果键不存在,则会创建新键。

int dbAdd(redisDb *db, robj *key, robj *val) {
    // 尝试插入新键值对到字典中
    if (dictAdd(db->dict, key, val) == DICT_OK) {
        return 1; // 成功
    } else {
        return 0; // 失败
    }
}
删除键

删除键可以通过 DEL 命令实现。如果键存在,则从键空间字典中移除该键。

int dbDelete(redisDb *db, robj *key) {
    if (dictDelete(db->dict, key) == DICT_OK) {
        return 1; // 成功
    } else {
        return 0; // 失败
    }
}
更新键

更新键值通常也是通过 SETHSET 等命令完成。如果键已经存在,则更新其值。

void setKey(redisDb *db, robj *key, robj *val) {
    if (lookupKeyWrite(db, key) == NULL) {
        dbAdd(db, key, val);
    } else {
        dbOverwrite(db, key, val);
    }
}
对键取值

通过 GET 命令等可以获取键的值。如果键存在,则返回其值。

robj *lookupKeyRead(redisDb *db, robj *key) {
    return dictFetchValue(db->dict, key);
}
其他键空间操作

包括键重命名、键移动等操作。

读写键空间时的维护操作

如键空间压缩、键空间清理等操作确保键空间高效运行。

数据结构图

以下是 Redis 键空间的结构图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其他说明

  • 键空间操作是 Redis 性能的关键,字典的高效实现保证了操作的快速响应。
  • 对键空间的每次写操作都会触发相应的维护操作,如过期键检查等。
  1. 设置见得生存时间或过期时间

概述

Redis 支持为键设置生存时间(TTL)或过期时间。当键的生存时间到期后,键会被自动删除。相关操作包括设置过期时间、保存过期时间、移除过期时间、计算并返回剩余生存时间、以及过期键的判定。

具体操作
设置过期时间

可以通过 EXPIREPEXPIREEXPIREATPEXPIREAT 等命令为键设置过期时间。

int setExpire(redisDb *db, robj *key, long long when) {
    if (dictAdd(db->expires, key, when) == DICT_OK) {
        return 1; // 成功
    } else {
        return 0; // 失败
    }
}
保存过期时间

过期时间以毫秒为单位存储在 expires 字典中,键为实际的键,值为过期时间戳。

移除过期时间

可以通过 PERSIST 命令移除键的过期时间。

int removeExpire(redisDb *db, robj *key) {
    if (dictDelete(db->expires, key) == DICT_OK) {
        return 1; // 成功
    } else {
        return 0; // 失败
    }
}
计算并返回剩余生存时间

通过 TTLPTTL 命令可以计算并返回键的剩余生存时间。

long long getExpire(redisDb *db, robj *key) {
    long long when = dictFetchValue(db->expires, key);
    return when - mstime();
}
过期键的判定

通过检查当前时间与键的过期时间来判定键是否过期。

int keyIsExpired(redisDb *db, robj *key) {
    long long when = dictFetchValue(db->expires, key);
    return mstime() > when;
}
数据结构图

以下是设置和管理键的过期时间的结构图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其他说明

  • 过期时间以毫秒为单位存储,确保精确的过期管理。
  • Redis 使用惰性删除和定期删除策略处理过期键,确保系统性能。
  1. 过期键删除策略

概述

Redis 使用三种主要策略来删除过期键:定时删除、惰性删除和定期删除。这些策略共同确保过期键能够及时删除,同时尽量减少对性能的影响。

策略详解
定时删除

定时删除(timed deletion)指的是在设置键的过期时间时,创建一个定时任务,在过期时间到达时立即删除该键。这种策略能够保证过期键能够及时删除,但会占用系统资源,尤其是当有大量键设置了过期时间时,可能导致系统性能下降。

Redis 并未使用这种策略,因为其开销较大,不适合高性能的要求。

惰性删除

惰性删除(lazy deletion)指的是在访问键时,检查键是否过期,如果过期则删除。这样做能够减少系统开销,但不能保证过期键能够及时删除。

实现惰性删除的关键代码:

if (keyIsExpired(db, key)) {
    dbDelete(db, key);
}
定期删除

定期删除(periodic deletion)指的是以固定时间间隔随机检查一部分键,并删除其中已经过期的键。这种策略能够在减少系统开销的同时,尽量保证过期键能够及时删除。

实现定期删除的关键代码:

void activeExpireCycle(int type) {
    // 定期检查键空间中的键是否过期,并进行删除
    // 略去具体实现代码
}
数据结构图

以下是三种过期键删除策略的结构图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其他说明

  • 定时删除不适合高性能场景,因此 Redis 未采用该策略。
  • 惰性删除能够减少系统开销,但不能保证及时删除。
  • 定期删除在保证性能的同时,尽量减少过期键存在时间。
  1. Redis的过期键删除策略

概述

Redis 实际上采用惰性删除和定期删除两种策略来处理过期键。这两种策略的结合使得 Redis 在保证性能的同时,尽量及时删除过期键。

惰性删除策略的实现

惰性删除策略在每次访问键时检查其是否过期,如果过期则删除。这种方法的实现主要在键访问的相关代码中:

robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
    robj *val = lookupKey(db, key);
    if (val == NULL) return NULL;

    // 检查键是否过期
    if (keyIsExpired(db, key)) {
        // 删除过期键
        dbDelete(db, key);
        return NULL;
    }
    return val;
}

每次通过 lookupKey 函数访问键时,都会调用 keyIsExpired 函数检查键是否过期。如果过期,则调用 dbDelete 函数删除键。

定期删除策略的实现

定期删除策略通过定期检查数据库中的键,删除其中已经过期的键。这部分代码实现了一个后台任务,定期执行过期键删除操作:

void activeExpireCycle(int type) {
    // 定期删除策略的实现
    static unsigned int current_db = 0;
    static int timelimit_exit = 0;
    long long start = ustime();
    long long timelimit;

    timelimit = 1000 * 1000 / server.hz; // 每秒执行的时间限制

    // 遍历数据库
    for (int j = 0; j < server.dbnum; j++) {
        redisDb *db = server.db + current_db;
        int expired;
        do {
            // 随机抽取一部分键进行检查
            expired = activeExpireCycleTryExpire(db);
            if (ustime() - start > timelimit) {
                timelimit_exit = 1;
            }
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP && !timelimit_exit);
        
        current_db++;
        current_db %= server.dbnum;
    }
}

该函数主要完成以下任务:

  1. 设定每次执行的时间限制,防止过期键检查占用过多资源。
  2. 遍历所有数据库,随机抽取一部分键进行检查。
  3. 检查键是否过期,如果过期则删除。
数据结构图

以下是 Redis 过期键删除策略的结构图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其他说明

  • 惰性删除确保了每次访问键时都能检查其是否过期,但不会主动触发。
  • 定期删除通过后台任务,定期对键空间进行检查,确保系统性能的同时尽量删除过期键。
  1. AOF,RDB和复制功能对过期键的处理

概述

Redis 提供了持久化和复制功能,包括 AOF(Append-Only File)、RDB(Redis Database Backup)和复制。每种功能对过期键的处理有所不同。

生成 RDB 文件

RDB 文件是 Redis 的持久化机制之一,它会在一定时间间隔内生成数据库的快照。过期键在生成 RDB 文件时可能会被处理如下:

  • 快照创建:在创建 RDB 文件时,Redis 会将当前数据库的所有键(包括未过期和已过期的键)保存到 RDB 文件中。
  • 过期键处理:在保存快照时,Redis 不会检查过期键。它只会将当前数据库中的所有键写入文件。因此,RDB 文件中可能包含过期键。

生成 RDB 文件的关键代码:

void rdbSave(char *filename) {
    // 创建 RDB 文件并写入数据库快照
    // 略去具体实现代码
}
载入 RDB 文件

当 Redis 启动时,会载入 RDB 文件中的数据。如果 RDB 文件中包含过期键,Redis 会在加载数据后处理这些键:

  • 数据加载:Redis 会从 RDB 文件中恢复所有键,包括过期键。
  • 过期键处理:在数据加载完成后,Redis 会根据过期时间检查并删除过期键。

载入 RDB 文件的关键代码:

void rdbLoad(char *filename) {
    // 读取 RDB 文件并恢复数据库数据
    // 略去具体实现代码
}
AOF 文件写入

AOF 文件是 Redis 的另一种持久化机制,它记录了所有对数据库的写操作。对过期键的处理如下:

  • 操作记录:当对键进行写操作时(包括设置过期时间),操作会被记录到 AOF 文件中。
  • 过期键处理:在 AOF 文件中,过期键的操作也会被记录。Redis 会根据 AOF 文件中的操作恢复键状态。

AOF 文件写入的关键代码:

void aofRewrite() {
    // 执行 AOF 重写操作,优化 AOF 文件
    // 略去具体实现代码
}
AOF 重写

AOF 重写是优化 AOF 文件的操作,减少文件的大小。重写过程中:

  • 数据恢复:AOF 重写会根据当前数据库状态重新生成 AOF 文件。
  • 过期键处理:在重写过程中,过期键会被忽略,不会写入新的 AOF 文件中。
复制

Redis 支持主从复制,将主节点的数据复制到从节点。在复制过程中,过期键的处理如下:

  • 数据同步:主节点的数据,包括过期键,会被同步到从节点。
  • 过期键处理:从节点会接收到主节点的过期键操作,并按照主节点的状态进行同步和处理。
数据结构图

以下是 AOF、RDB 和复制功能对过期键的处理的结构图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其他说明

  • RDB 和 AOF 都可能包含过期键,但 Redis 在恢复数据后会处理这些过期键。
  • 复制过程会同步主节点的所有数据,包括过期键,确保从节点与主节点一致。
  1. 数据库通知

概述

Redis 支持键空间通知功能,允许客户端订阅特定的键事件,以便在键发生变化时获得通知。这对于实现缓存失效、实时监控等功能非常有用。

发送通知

Redis 的键空间通知通过 PSUBSCRIBESUBSCRIBE 命令进行订阅。要发送通知,需要启用键空间通知功能,并在数据库操作时触发相关事件。

启用通知

可以通过配置文件或 CONFIG SET 命令启用键空间通知。通知类型可以是过期、删除、修改等:

CONFIG SET notify-keyspace-events "Ex"

这里 "Ex" 表示启用过期和删除事件通知。

触发通知

在执行对键的操作(如设置、删除、过期)时,Redis 会触发相应的通知:

void signalModifiedKey(redisDb *db, robj *key) {
    if (server.notifications & REDIS_NOTIFY_GENERIC) {
        publishKeyspaceEvent(REDIS_NOTIFY_GENERIC, "del", key, db->id);
    }
}

在执行键的删除操作时,signalModifiedKey 函数会根据配置的通知类型,发布相应的事件。

发送通知的实现

Redis 使用发布-订阅模式来实现键空间通知。主要的实现涉及到事件的生成和分发:

  1. 事件生成:在操作键时(如 DELSET),Redis 会根据当前的通知配置生成相应的事件。
  2. 事件分发:Redis 使用 Pub/Sub 机制将事件广播给订阅的客户端。

发送通知的关键代码:

void publishKeyspaceEvent(int type, const char *event, robj *key, int dbid) {
    robj *channel = createStringObject("notify-keyspace-events", 
                                        strlen("notify-keyspace-events"));
    robj *message = createStringObject(event, strlen(event));
    robj *data = createStringObject(key->ptr, sdslen(key->ptr));

    listAddNodeTail(server.pubsub_channels, createPubsubMessage(channel, message, data));

    // 触发通知
    signalModifiedKey(server.db, key);
}
数据结构图

以下是数据库通知功能的结构图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其他说明

  • 启用键空间通知会增加 Redis 的负担,因此需要根据实际需要合理配置。
  • 键空间通知的事件类型可以配置,包括过期、删除和修改等。

RDB持久化

  1. RDB文件的创建和载入

SAVE命令执行时的服务器状态
  • SAVE命令SAVE 是一个阻塞操作,会立即创建 RDB 文件,阻塞 Redis 服务器,直到文件创建完成。
    • 操作流程
      • 阻塞服务器:在执行 SAVE 命令时,Redis 会停止接受和处理其他客户端请求,直到 RDB 文件创建完成。
      • 创建 RDB 文件:Redis 服务器会遍历数据库中的所有键值对,并将这些数据写入到 RDB 文件中。
      • 恢复处理:创建完成后,Redis 恢复正常处理客户端请求。
// 示例:SAVE命令的伪代码
func saveRDB(filename string) {
    // 开始阻塞
    lockServer()
    defer unlockServer()
    
    // 创建 RDB 文件
    createRDBFile(filename)
    
    // 遍历数据库并写入数据
    for _, db := range databases {
        for _, key := range db.keys {
            writeToRDBFile(key, db.getValue(key))
        }
    }
}
  • 影响
    • 性能:由于 SAVE 操作会阻塞 Redis 服务器,因此在高负载环境下使用可能会影响系统性能。
    • 适用场景:适用于不需要实时处理请求的场景,如在维护期间生成备份。
BGSAVE命令执行时的服务器状态
  • BGSAVE命令BGSAVE 命令触发 Redis 在后台生成 RDB 文件,不会阻塞主线程,使 Redis 能够继续处理客户端请求。
    • 操作流程
      • 触发****子进程BGSAVE 命令会创建一个子进程,该子进程负责生成 RDB 文件。
      • 主进程继续运行:主进程继续处理客户端请求,不受影响。
      • 子进程****执行:子进程会执行类似于 SAVE 命令的操作,遍历数据库中的所有键值对,并将其写入 RDB 文件。
      • 完成后通知:子进程完成 RDB 文件的创建后会退出,Redis 服务器主进程会收到通知。
// 示例:BGSAVE命令的伪代码
func bgsaveRDB(filename string) {
    // 创建子进程
    pid := fork()
    if pid == 0 {
        // 子进程执行 RDB 文件生成
        createRDBFile(filename)
        exit(0)
    } else {
        // 主进程继续运行
        return
    }
}
  • 影响
    • 性能BGSAVE 命令的使用不会阻塞主线程,因此可以在高负载的环境下使用,不会影响正常的客户端请求处理。
    • 适用场景:适用于需要高可用性和实时处理请求的环境。
RDB文件载入时的服务器状态
  • RDB****文件载入:在 Redis 启动时,如果存在 RDB 文件,Redis 会自动载入该文件以恢复数据库的状态。
    • 操作流程
      • 读取 RDB 文件:Redis 会打开 RDB 文件并读取其内容。
      • 恢复数据
        1. 清空当前数据库:在载入 RDB 文件之前,Redis 会清空当前数据库中的所有数据。
        2. 解析和恢复:Redis 解析 RDB 文件中的数据,逐个恢复数据库中的键值对。
      • 更新状态:恢复完成后,Redis 更新数据库的内部状态,并开始正常处理客户端请求。
// 示例:RDB文件载入的伪代码
func loadRDB(filename string) {
    // 打开 RDB 文件
    file := openFile(filename)
    
    // 清空当前数据库
    clearDatabase()
    
    // 读取和恢复数据
    for {
        key, value := readNextEntry(file)
        if key == nil {
            break
        }
        setKeyValue(key, value)
    }
    
    // 关闭文件
    closeFile(file)
}
  • 影响
    • 启动时间:载入 RDB 文件的时间取决于数据库的大小和 RDB 文件的大小,可能会影响 Redis 启动的时间。
    • 数据一致性:恢复过程中的数据会覆盖现有数据,因此需要确保 RDB 文件是最新的,以避免数据丢失或不一致。
数据结构图

以下是 RDB 文件载入过程的结构图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其他说明

  • RDB 文件载入是 Redis 启动过程的一部分,确保服务器能够从持久化文件中恢复数据。
  • 为了避免在恢复过程中影响服务的可用性,建议使用 RDB 文件的最新快照,并确保其完整性。
  1. 自动间隔性保存

Redis 支持自动间隔性保存,通过定期生成 RDB 文件来持久化数据。这有助于确保在系统崩溃或重启后,数据不会丢失太多。

设置保存时间

Redis 配置文件中有 save 选项来设置自动保存的条件。该选项定义了 Redis 在满足特定条件时自动创建 RDB 文件。例如:

复制代码
save 900 1
save 300 10
save 60 10000

这些配置表示:

  • 每 900 秒(15 分钟),如果至少有 1 个键被修改,则创建 RDB 文件。
  • 每 300 秒(5 分钟),如果至少有 10 个键被修改,则创建 RDB 文件。
  • 每 60 秒(1 分钟),如果至少有 10,000 个键被修改,则创建 RDB 文件。
dirty计数器和lastsave属性
  • dirty计数器
    • 概念dirty 是 Redis 中的一个内部计数器,用于记录自上次 RDB 保存以来,数据库中键的修改次数。
    • 作用dirty 计数器用于检查是否需要创建新的 RDB 文件。当计数器达到配置中的阈值时,Redis 会触发 RDB 保存。
  • lastsave属性
    • 概念lastsave 是 Redis 中的一个属性,记录上次 RDB 文件创建的时间戳。
    • 作用lastsave 用于计算自上次保存以来经过的时间,并与自动保存的时间间隔进行比较,以决定是否需要创建新的 RDB 文件。
// 示例:自动保存的伪代码
func autoSave() {
    if time.Now().Sub(lastSaveTime) >= saveInterval && dirty > saveThreshold {
        saveRDB("dump.rdb")
        lastSaveTime = time.Now()
        dirty = 0
    }
}
检查保存条件是否满足

Redis 会定期检查是否满足自动保存的条件。具体过程如下:

  1. 检查时间间隔:通过比较当前时间和 lastsave 时间,判断是否满足时间间隔。
  2. 检查修改次数:通过检查 dirty 计数器,判断是否满足键修改次数的条件。
  3. 触发保存:如果满足条件,执行 RDB 保存操作并重置 dirty 计数器和 lastsave 时间。
// 示例:检查保存条件的伪代码
func checkSaveConditions() {
    if time.Since(lastSaveTime) >= saveInterval && dirty > saveThreshold {
        bgsaveRDB("dump.rdb")
    }
}
数据结构图

以下是自动间隔性保存的过程图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其他说明

  • 自动保存机制可以帮助防止数据丢失,但频繁保存可能会影响性能。
  • 在高负载环境中,可以调整保存条件以平衡性能和数据安全性。
  1. RDB文件结构

RDB 文件包含了 Redis 数据库的快照,保存了数据库中所有键值对的状态。RDB 文件的结构设计目的是为了高效地存储和恢复大量数据。

databases部分
  • 概述:RDB 文件中的 databases 部分存储了 Redis 数据库的整体状态,包括每个数据库的键值对。
  • 格式
    • 数据库数量:RDB 文件开头部分存储了 Redis 实例中数据库的数量。
    • 每个数据库的键:对于每个数据库,RDB 文件接下来存储了该数据库中的所有键值对。
key_value_pairs部分
  • 概述key_value_pairs 部分存储了数据库中的所有键值对。
  • 格式
    • :每个键的存储格式包括键的长度和键的内容。
    • :每个值的存储格式包括值的长度和值的内容。
value编码

Redis 采用多种编码方式来高效存储不同类型的数据。RDB 文件会对值进行不同的编码。

字符串对象
  • 编码:字符串对象直接存储字符串的内容和长度。
  • 格式
    • 简单字符串:直接存储字符串值。
    • 压缩字符串:使用 LZF 压缩算法进行压缩。
列表对象
  • 编码:列表对象的值可以采用以下编码方式:
    • ziplist:适用于小型列表,将所有元素以紧凑格式存储。
    • linkedlist:用于较大的列表,每个元素作为节点连接形成链表。
集合对象
  • 编码:集合对象的值可以采用以下编码方式:
    • intset:用于存储整数集合,采用压缩存储。
    • hashtable:用于存储字符串集合,采用哈希表存储。
哈希表对象
  • 编码:哈希表对象的值可以采用以下编码方式:
    • ziplist:用于小型哈希表,存储键值对的紧凑格式。
    • hashtable:用于大型哈希表,采用哈希表存储。
有序集合对象
  • 编码:有序集合对象的值可以采用以下编码方式:
    • ziplist:用于小型有序集合,采用紧凑存储。
    • skiplist:用于大型有序集合,使用跳表结构进行存储。
数据结构图

以下是 RDB 文件结构的示意图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其他说明

  • RDB 文件结构设计以高效存储和快速恢复为目标。
  • 不同类型的数据使用不同的编码方式,以提高存储效率。

4.分析RDB文件

RDB 文件用于保存 Redis 数据库的快照。分析 RDB 文件可以帮助理解 Redis 的持久化机制和文件结构。以下是对 RDB 文件内容的不同场景分析。

不包含任何键值对的RDB文件
  • 概述:如果 RDB 文件在保存时数据库为空,则文件中不会包含任何键值对。
  • 结构
    • 文件头:包含 RDB 文件的元数据,如文件版本、数据库数量等。
    • 数据库部分:虽然包含数据库信息,但由于没有键值对,因此数据库部分将为空。
  • 示例
    • 场景:Redis 实例刚启动且没有任何键被设置时,生成的 RDB 文件会表现为这种情况。
包含字符串键的RDB文件
  • 概述:包含字符串键的 RDB 文件会存储每个字符串键及其对应的值。
  • 结构
    • 文件头:包含文件的元数据。
    • databases部分:记录数据库的状态和数量。
    • key_value_pairs部分:包括字符串键和它们的值。
    • value编码:字符串值的存储格式。
  • 示例
    • 场景:Redis 中包含键值对,例如 SET foo bar,在 RDB 文件中,foo 会作为一个字符串键存在,bar 作为其值。
包含带有过期时间的字符串键的RDB文件
  • 概述:RDB 文件中的过期时间存储了键的有效期,超时的键会在文件中包含过期时间。
  • 结构
    • 文件头:包含文件的元数据。
    • databases部分:记录数据库的状态和数量。
    • key_value_pairs部分:包括字符串键及其值。
    • 过期时间:每个键可能会有一个过期时间戳。
  • 示例
    • 场景:如果 SET foo bar EX 60 被执行,foo 的过期时间将存储在 RDB 文件中。
包含一个集合键的RDB文件
  • 概述:RDB 文件中包含的集合键会记录集合的成员和编码方式。
  • 结构
    • 文件头:包含文件的元数据。
    • databases部分:记录数据库的状态和数量。
    • key_value_pairs部分:包括集合键及其成员。
    • value编码:集合的编码方式,如 intsethashtable
  • 示例
    • 场景:如果 SADD myset a b c 被执行,myset 的成员 a, b, c 会被存储在 RDB 文件中。
关于分析RDB文件的说明
  • 工具:可以使用 redis-rdb-tools 等工具来分析和查看 RDB 文件的内容。
  • 内容解码:根据文件中的编码方式,需要对不同类型的值进行解码,如字符串、集合等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其他说明

  • RDB 文件的结构和内容直接影响数据恢复的效率和准确性。
  • 理解 RDB 文件的结构有助于优化 Redis 的持久化配置和数据恢复策略。

AOF持久化

  1. AOF持久化的实现

命令追加
  • 概述:AOF 持久化的核心在于将 Redis 的所有写命令按顺序追加到 AOF 文件中。这使得 Redis 可以通过重放 AOF 文件中的命令来恢复数据。
  • 操作流程
    • 写命令格式:每个写命令以其原始格式(即 Redis 命令的文本格式)追加到 AOF 文件中。例如,SET foo bar 命令会被追加为 SET foo bar
    • 追加操作:在 Redis 执行写操作时,除了将数据更改到内存中,还会将命令追加到 AOF 文件末尾。
// 示例:将命令追加到 AOF 文件的伪代码
func appendToAOF(command string) {
    file, err := os.OpenFile("appendonly.aof", os.O_APPEND|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    if _, err := file.WriteString(command + "\n"); err != nil {
        log.Fatal(err)
    }
}
  • 重点解析
    • 命令格式:AOF 文件中的命令格式和 Redis 客户端使用的格式相同,这样可以确保在恢复时能够正确地重放这些命令。
    • 追加操作:追加操作使得 Redis 可以高效地记录每一个写命令,而不需要重新生成整个数据集。这种方法简化了数据持久化的逻辑,但也可能导致 AOF 文件变得非常大。
AOF文件的写入和同步
  • 写入:每次写操作都会立即追加到 AOF 文件中,确保数据不丢失。
  • 同步策略
    • 每次写入:每次写入操作后立即将文件同步到磁盘。虽然数据丢失风险最低,但会影响性能。
    • 定期同步:Redis 可以配置为每秒同步一次 AOF 文件。这样在系统崩溃时最多丢失最近一秒的数据。
    • 后台异步:使用后台线程异步地将数据写入磁盘,可以提高性能,但可能会导致在崩溃时丢失最近的数据。
// 示例:同步 AOF 文件的伪代码
func syncAOF(syncMode string) {
    file, err := os.OpenFile("appendonly.aof", os.O_RDWR, 0644)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    switch syncMode {
    case "every":
        file.Sync() // 每次写入后立即同步
    case "no":
        // 不同步,依赖操作系统缓存
    case "always":
        // 始终同步,性能较差
    }
}
  • 重点解析
    • 同步策略
      • 每次写入:保证最高的数据安全性,但可能会显著降低 Redis 的性能。
      • 定期同步:在性能和数据安全性之间取得平衡,适用于大多数生产环境。
      • 后台异步:提高性能,但在极端情况下可能会导致数据丢失。
  1. AOF文件的载入与数据还原

AOF文件载入的过程
  • 概述:Redis 在启动时加载 AOF 文件以恢复数据,确保系统在重启后能恢复到崩溃前的状态。
  1. 读取 AOF 文件
  • 操作:Redis 启动时会检查并打开 AOF 文件。
  • 代码示例
// 示例:打开 AOF 文件的伪代码
func openAOF(filename string) (*os.File, error) {
    return os.Open(filename)
}
  • 重点解析
    • 文件检查:确保文件存在且可读。
  1. 清空数据库
  • 操作:在加载 AOF 文件之前,Redis 会清空当前数据库,以确保不会将旧数据与新数据混合。
  • 代码示例
// 示例:清空数据库的伪代码
func clearDatabase() {
    // 清空数据库逻辑
}
  • 重点解析
    • 数据隔离:防止旧数据影响恢复过程。
  1. 重放命令
  • 操作:逐行读取 AOF 文件中的命令,并按顺序执行这些命令来恢复数据。
  • 代码示例
// 示例:逐行读取和执行命令的伪代码
func replayCommands(file *os.File) error {
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        command := scanner.Text()
        if err := executeCommand(command); err != nil {
            return err
        }
    }
    return scanner.Err()
}

func executeCommand(command string) error {
    // 执行命令的逻辑
    return nil
}
  • 重点解析
    • 命令顺序:保证文件中的命令按顺序执行,确保数据一致性。
    • 错误处理:处理文件读取和命令执行中的潜在错误。
  1. AOF重写

1. AOF文件重写的实现
  • 概述:AOF 文件重写的过程包括创建一个新的 AOF 文件,重写过程中的所有写操作都不会影响新的 AOF 文件。
  • 操作流程
    • 触发重写:当 AOF 文件大小达到设定阈值时,Redis 会触发 AOF 重写。
    • 创建新文件:Redis 会创建一个新的 AOF 文件用于存储重写后的数据。
    • 重写过程
      1. Redis 会遍历当前数据库中的所有数据,并将这些数据以最小化的命令形式写入新的 AOF 文件。
      2. 在重写过程中,Redis 会继续将写命令追加到旧的 AOF 文件,以避免数据丢失。
    • 完成重写:重写完成后,Redis 会将新 AOF 文件替换旧文件。
// 示例:AOF 重写的伪代码
func rewriteAOF() error {
    newFile, err := os.Create("appendonly-new.aof")
    if err != nil {
        return err
    }
    defer newFile.Close()

    // 遍历数据库中的所有数据,并写入新文件
    if err := writeDatabaseToAOF(newFile); err != nil {
        return err
    }

    // 替换旧的 AOF 文件
    return os.Rename("appendonly-new.aof", "appendonly.aof")
}

func writeDatabaseToAOF(file *os.File) error {
    // 遍历和写入数据的逻辑
    return nil
}
  • 重点解析
    • 触发条件:AOF 重写的触发条件通常是 AOF 文件的大小超过一定阈值,或者在特定时间间隔内自动触发。
    • 新文件创建:重写过程中创建的新文件会减少 AOF 文件的大小,提高性能。
    • 原子替换:重写完成后,使用原子操作替换旧文件,确保数据一致性和可靠性。
2. AOF后台重写
  • 概述:AOF 背景重写是在后台线程中执行的,以避免阻塞主线程,从而减少对 Redis 响应能力的影响。
  • 操作流程
    • 启动后台进程:Redis 启动一个后台进程执行 AOF 重写操作。
    • 同步数据:在后台进程中,Redis 同步当前数据库的数据到新的 AOF 文件。
    • 数据同步:在后台重写过程中,主线程会继续接受和处理客户端请求。
    • 完成重写:重写完成后,将新 AOF 文件替换旧文件,保证数据的持久化。
// 示例:后台重写的伪代码
func backgroundRewriteAOF() {
    go func() {
        if err := rewriteAOF(); err != nil {
            log.Println("AOF rewrite failed:", err)
        }
    }()
}
  • 重点解析
    • 后台处理:使用后台进程处理 AOF 重写,避免主线程阻塞,提升 Redis 的性能。
    • 并发操作:后台进程和主线程可以并发执行,确保 Redis 服务的持续可用性。

这些步骤和机制确保 AOF 文件能够有效地重写,减少其大小并提高性能,同时保持数据的可靠性。

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值