数据库
文章目录
- 数据库
- RDB持久化
- AOF持久化
-
- AOF持久化的实现
- AOF文件的载入与数据还原
- AOF重写
-
-
-
- [1. AOF文件重写的实现](#1. AOF文件重写的实现)
- [2. AOF后台重写](#2. AOF后台重写)
-
-
服务器中的数据库
概述
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 中添加新键通常通过 SET
、HSET
等命令完成。如果键不存在,则会创建新键。
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; // 失败
}
}
更新键
更新键值通常也是通过 SET
、HSET
等命令完成。如果键已经存在,则更新其值。
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)或过期时间。当键的生存时间到期后,键会被自动删除。相关操作包括设置过期时间、保存过期时间、移除过期时间、计算并返回剩余生存时间、以及过期键的判定。
具体操作
设置过期时间
可以通过 EXPIRE
、PEXPIRE
、EXPIREAT
、PEXPIREAT
等命令为键设置过期时间。
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; // 失败
}
}
计算并返回剩余生存时间
通过 TTL
或 PTTL
命令可以计算并返回键的剩余生存时间。
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;
}
}
该函数主要完成以下任务:
- 设定每次执行的时间限制,防止过期键检查占用过多资源。
- 遍历所有数据库,随机抽取一部分键进行检查。
- 检查键是否过期,如果过期则删除。
数据结构图
以下是 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 的键空间通知通过 PSUBSCRIBE
和 SUBSCRIBE
命令进行订阅。要发送通知,需要启用键空间通知功能,并在数据库操作时触发相关事件。
启用通知
可以通过配置文件或 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 使用发布-订阅模式来实现键空间通知。主要的实现涉及到事件的生成和分发:
- 事件生成 :在操作键时(如
DEL
、SET
),Redis 会根据当前的通知配置生成相应的事件。 - 事件分发: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 文件并读取其内容。
- 恢复数据 :
- 清空当前数据库:在载入 RDB 文件之前,Redis 会清空当前数据库中的所有数据。
- 解析和恢复: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 会定期检查是否满足自动保存的条件。具体过程如下:
- 检查时间间隔 :通过比较当前时间和
lastsave
时间,判断是否满足时间间隔。 - 检查修改次数 :通过检查
dirty
计数器,判断是否满足键修改次数的条件。 - 触发保存 :如果满足条件,执行 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
作为其值。
- 场景 :Redis 中包含键值对,例如
包含带有过期时间的字符串键的RDB文件
- 概述:RDB 文件中的过期时间存储了键的有效期,超时的键会在文件中包含过期时间。
- 结构 :
- 文件头:包含文件的元数据。
- databases部分:记录数据库的状态和数量。
- key_value_pairs部分:包括字符串键及其值。
- 过期时间:每个键可能会有一个过期时间戳。
- 示例 :
- 场景 :如果
SET foo bar EX 60
被执行,foo
的过期时间将存储在 RDB 文件中。
- 场景 :如果
包含一个集合键的RDB文件
- 概述:RDB 文件中包含的集合键会记录集合的成员和编码方式。
- 结构 :
- 文件头:包含文件的元数据。
- databases部分:记录数据库的状态和数量。
- key_value_pairs部分:包括集合键及其成员。
- value编码 :集合的编码方式,如
intset
或hashtable
。
- 示例 :
- 场景 :如果
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 文件末尾。
- 写命令格式 :每个写命令以其原始格式(即 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 文件以恢复数据,确保系统在重启后能恢复到崩溃前的状态。
- 读取 AOF 文件
- 操作:Redis 启动时会检查并打开 AOF 文件。
- 代码示例:
Go
// 示例:打开 AOF 文件的伪代码
func openAOF(filename string) (*os.File, error) {
return os.Open(filename)
}
- 重点解析 :
- 文件检查:确保文件存在且可读。
- 清空数据库
- 操作:在加载 AOF 文件之前,Redis 会清空当前数据库,以确保不会将旧数据与新数据混合。
- 代码示例:
Go
// 示例:清空数据库的伪代码
func clearDatabase() {
// 清空数据库逻辑
}
- 重点解析 :
- 数据隔离:防止旧数据影响恢复过程。
- 重放命令
- 操作:逐行读取 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 文件用于存储重写后的数据。
- 重写过程 :
- Redis 会遍历当前数据库中的所有数据,并将这些数据以最小化的命令形式写入新的 AOF 文件。
- 在重写过程中,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 文件能够有效地重写,减少其大小并提高性能,同时保持数据的可靠性。