单机数据库的实现(上)

数据库

文章目录

服务器中的数据库

概述

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

实现

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

C 复制代码
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 命令。

切换数据库

概述

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

实现

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

C 复制代码
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 函数用于实际切换数据库:

C 复制代码
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 命令可以查看各个数据库的键数量和过期信息。

数据库键空间

概述

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

具体操作
添加新键

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

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

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

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

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

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

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

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

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

读写键空间时的维护操作

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

数据结构图

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

其他说明

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

设置见得生存时间或过期时间

概述

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

具体操作
设置过期时间

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

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

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

移除过期时间

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

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

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

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

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

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

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

其他说明

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

过期键删除策略

概述

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

策略详解
定时删除

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

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

惰性删除

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

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

C 复制代码
if (keyIsExpired(db, key)) {
    dbDelete(db, key);
}
定期删除

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

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

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

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

其他说明

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

Redis的过期键删除策略

概述

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

惰性删除策略的实现

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

C 复制代码
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 函数删除键。

定期删除策略的实现

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

C 复制代码
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 过期键删除策略的结构图:

其他说明

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

AOF,RDB和复制功能对过期键的处理

概述

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

生成 RDB 文件

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

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

生成 RDB 文件的关键代码:

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

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

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

载入 RDB 文件的关键代码:

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

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

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

AOF 文件写入的关键代码:

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

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

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

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

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

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

其他说明

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

数据库通知

概述

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

发送通知

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

启用通知

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

Bash 复制代码
CONFIG SET notify-keyspace-events "Ex"

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

触发通知

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

C 复制代码
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 机制将事件广播给订阅的客户端。

发送通知的关键代码:

C 复制代码
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持久化

RDB文件的创建和载入

SAVE命令执行时的服务器状态
  • SAVE命令SAVE 是一个阻塞操作,会立即创建 RDB 文件,阻塞 Redis 服务器,直到文件创建完成。
    • 操作流程
      • 阻塞服务器 :在执行 SAVE 命令时,Redis 会停止接受和处理其他客户端请求,直到 RDB 文件创建完成。
      • 创建 RDB 文件:Redis 服务器会遍历数据库中的所有键值对,并将这些数据写入到 RDB 文件中。
      • 恢复处理:创建完成后,Redis 恢复正常处理客户端请求。
Go 复制代码
// 示例: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 服务器主进程会收到通知。
Go 复制代码
// 示例: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 更新数据库的内部状态,并开始正常处理客户端请求。
Go 复制代码
// 示例: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 文件的最新快照,并确保其完整性。

自动间隔性保存

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

设置保存时间

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

Plaintext 复制代码
复制代码
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 文件。
Go 复制代码
// 示例:自动保存的伪代码
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 时间。
Go 复制代码
// 示例:检查保存条件的伪代码
func checkSaveConditions() {
    if time.Since(lastSaveTime) >= saveInterval && dirty > saveThreshold {
        bgsaveRDB("dump.rdb")
    }
}
数据结构图

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

其他说明

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

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持久化

AOF持久化的实现

命令追加
  • 概述:AOF 持久化的核心在于将 Redis 的所有写命令按顺序追加到 AOF 文件中。这使得 Redis 可以通过重放 AOF 文件中的命令来恢复数据。
  • 操作流程
    • 写命令格式 :每个写命令以其原始格式(即 Redis 命令的文本格式)追加到 AOF 文件中。例如,SET foo bar 命令会被追加为 SET foo bar
    • 追加操作:在 Redis 执行写操作时,除了将数据更改到内存中,还会将命令追加到 AOF 文件末尾。
Go 复制代码
// 示例:将命令追加到 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 文件。这样在系统崩溃时最多丢失最近一秒的数据。
    • 后台异步:使用后台线程异步地将数据写入磁盘,可以提高性能,但可能会导致在崩溃时丢失最近的数据。
Go 复制代码
// 示例:同步 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 的性能。
      • 定期同步:在性能和数据安全性之间取得平衡,适用于大多数生产环境。
      • 后台异步:提高性能,但在极端情况下可能会导致数据丢失。

AOF文件的载入与数据还原

AOF文件载入的过程
  • 概述:Redis 在启动时加载 AOF 文件以恢复数据,确保系统在重启后能恢复到崩溃前的状态。
  1. 读取 AOF 文件
  • 操作:Redis 启动时会检查并打开 AOF 文件。
  • 代码示例
Go 复制代码
// 示例:打开 AOF 文件的伪代码
func openAOF(filename string) (*os.File, error) {
    return os.Open(filename)
}
  • 重点解析
    • 文件检查:确保文件存在且可读。
  1. 清空数据库
  • 操作:在加载 AOF 文件之前,Redis 会清空当前数据库,以确保不会将旧数据与新数据混合。
  • 代码示例
Go 复制代码
// 示例:清空数据库的伪代码
func clearDatabase() {
    // 清空数据库逻辑
}
  • 重点解析
    • 数据隔离:防止旧数据影响恢复过程。
  1. 重放命令
  • 操作:逐行读取 AOF 文件中的命令,并按顺序执行这些命令来恢复数据。
  • 代码示例
Go 复制代码
// 示例:逐行读取和执行命令的伪代码
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
}
  • 重点解析
    • 命令顺序:保证文件中的命令按顺序执行,确保数据一致性。
    • 错误处理:处理文件读取和命令执行中的潜在错误。

AOF重写

1. AOF文件重写的实现
  • 概述:AOF 文件重写的过程包括创建一个新的 AOF 文件,重写过程中的所有写操作都不会影响新的 AOF 文件。
  • 操作流程
    • 触发重写:当 AOF 文件大小达到设定阈值时,Redis 会触发 AOF 重写。
    • 创建新文件:Redis 会创建一个新的 AOF 文件用于存储重写后的数据。
    • 重写过程
      1. Redis 会遍历当前数据库中的所有数据,并将这些数据以最小化的命令形式写入新的 AOF 文件。
      2. 在重写过程中,Redis 会继续将写命令追加到旧的 AOF 文件,以避免数据丢失。
    • 完成重写:重写完成后,Redis 会将新 AOF 文件替换旧文件。
Go 复制代码
// 示例: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 文件替换旧文件,保证数据的持久化。
Go 复制代码
// 示例:后台重写的伪代码
func backgroundRewriteAOF() {
    go func() {
        if err := rewriteAOF(); err != nil {
            log.Println("AOF rewrite failed:", err)
        }
    }()
}
  • 重点解析
    • 后台处理:使用后台进程处理 AOF 重写,避免主线程阻塞,提升 Redis 的性能。
    • 并发操作:后台进程和主线程可以并发执行,确保 Redis 服务的持续可用性。

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

相关推荐
水月梦镜花9 小时前
redis:list列表命令和内部编码
数据库·redis·list
掘金-我是哪吒10 小时前
微服务mysql,redis,elasticsearch, kibana,cassandra,mongodb, kafka
redis·mysql·mongodb·elasticsearch·微服务
ketil2712 小时前
Ubuntu 安装 redis
redis
王佑辉13 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
Karoku06614 小时前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
gorgor在码农14 小时前
Redis 热key总结
java·redis·热key
想进大厂的小王14 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
Java 第一深情15 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
minihuabei20 小时前
linux centos 安装redis
linux·redis·centos
monkey_meng1 天前
【Rust中多线程同步机制】
开发语言·redis·后端·rust