redis数据库
服务器的数据库
struct redisServer {
int dbnum;
redisDb *db;
};
每个redisDb代表一个数据库,dbnum表示服务器数据库个数。默认会创建16个,由database配置选项决定。
数据库键值对都是robj对象,
数据库切换
每个redis客户端都有一个目标数据库,可以通过SELECT选择,默认是db[0]数据库。
SELECT 2 即选择db[2]数据库
客户端状态redisClient种db属性记录目标数据库。但是从命令方面,客户端不知道自己正在使用哪个数据库。
键空间
redis是一个键值对数据库服务器,每个数据库由redisDb表示。
typedef struct redisDb {
dict *dict; // 键空间:保存所有键值对
} redisDb;
dict属性持有所有键值对,称为键空间。键即是字符串对象,值即是redis对象。 针对数据库增删改查操作,即是对字典dict的操作。
SET date "2023-01-10"
DEL date
还有一些是针对键空间操作,比如FLUSHDB会删除所有键值对,RANDOMKEY随机返回键值。 DBSIZE返回键值对数量,EXISTS,RENAME,KEYS等。
读写键空间时的维护操作
使用命令进行读写时候,还有一些额外维护操作:
- 读取键时候(读写都要读取键),会根据键是否存在更新INFO stats命令的keyspace_hits 和 keyspace_misses指标,这两个表示是否命中,即键是否在服务器。
- 读取键后,会更新键的LRU
- 读取键发现键过期们就会删除键,在执行。
- 如果客户端使用WATCH命令监视这个键,服务器修改键之后会将其标记为dirty,让事务程序注意。
- 服务器每次修改一个键,脏键计数器都会+1,之后会触发持久化或复制。
- 如果开启了数据库通知功能,键修改后,还当按配置发送通知。
键生存/过期
通过EXPIRE或者PEXPIRE(毫秒级)命令,客户端指定键的生存时间TTL,经过指定时间,服务器自动删除剩余时间为0的键。
类似,EXPIREAT或者PEXPIREAT表示在某个UNIX时间戳过期。
TTL命令和PTTL命令则返回键的剩余时间
设置过期时间的实现
都可以转化为PEXPIREAT执行。
### 数据库保存键过期时间
typedef struct redisDb {
// …
dict* expires; // 过期字典:保存所有键的过期时间。
} redisDb;
过期字典的键是一个指针,指向键空间的某个键。
过期字典的值是一个long long的整数,保存对应的过期时间——毫秒级的unix时间戳。
移除过期时间
PERSIST命令移除键的过期时间。会从过期字典移除。
过期键的判定
通过TTL和PTTL返回
删除策略
删除策略,已知一个键过期,如何删除?
- 定时删除:对键设置过期时间同时,创建定时器,定时器到了立即执行。
- 惰性删除:每次从键空间读键时候,过期则删除。
- 定期删除:每隔一段时间,数据库检查,删除过期键。
定时删除
优点:定时删除能及时释放内存。
缺点:
- 如果过期键比较多,同一时间会删除大量键,占用大量CPU时间。
- redis创建定时器需要时间时间,时间事件实现方式是无序链表,复杂度0(N),大量事件事件不能高效处理
事实上,如果有大量命令请求需要处理,又不缺少内存,不应该此时去处理过期键,而是优先命令请求。
综述:所以并不现实。
惰性删除
优点:对CPU友好,只在非做不可情况下去做,且可以直接删除当前处理键。
缺点:无用的垃圾数据会占用大量内存。
比如,记录一些log,记录后很少访问,内存大大浪费。
定期删除
定期删除策略为折中,但难度在于设置删除频率。
RDB持久化
RDB 格式
在 Redis RDB 文件中,文件标记(magic values)用于区分数据结构、类型和边界,确保存储和解析一致。选择合适的值通常遵循以下原则:
- 唯一性:确保标记值不会与正常数据冲突。(普通数据不会用到的字节)
- 可扩展性:预留空间,避免未来变更影响格式
- 易解析:选择固定字节长度,避免不必要的位运算
总结
标记 | 作用 | 典型取值 |
---|---|---|
文件头 | 版本识别 | REDIS0009 |
EOF | 结束标记 | 0xFF |
DB Selector | 选择数据库 | 0xFE + dbid |
Type | 数据类型 | 0x00 ~ 0x05 |
Encoding | 存储方式 | 0x00 ~ 0x03 |
可变长度编码 | 存储数据长度 | 0x00 ~ 0x80 |
特殊整数 | INT8/16/32 | 0xFC, 0xFD, 0xFE |
辅助信息 | 额外字段 | 0xFA |
这些值是 Redis 选择的标准,它们能确保 RDB 高效、可扩展、易解析。🚀
Redis BGSAVE
Redis 在 BGSAVE 过程中,使用 Linux 信号(signal)机制 来管理主进程和子进程的通信,确保 BGSAVE 进程状态能够被正确追踪。
- signal 机制在 Redis BGSAVE 的作用
Redis 采用 SIGCHLD 信号 监测 BGSAVE 进程的退出:
- 子进程 exit(0)(成功)或 exit(1)(失败)后,Linux 自动向父进程发送 SIGCHLD 信号。
- 父进程通过 SIGCHLD 处理函数 sigChildHandler() 识别 BGSAVE 的结果:
- 成功:更新 server.lastsave(最后成功 BGSAVE 的时间)。
- 失败:清理 server.rdb_child_pid,避免误判进程状态。
-
BGSAVE 触发流程
- Redis 主进程 调用 fork() 创建 BGSAVE 子进程:
- 子进程 执行 rdbSave() 写入 RDB 文件。
- 父进程 继续接收客户端请求,等待 SIGCHLD。
- 子进程执行 rdbSave() 完成后,调用 exit(0) 退出:
- 操作系统自动向父进程发送 SIGCHLD 信号。
- Redis 主进程收到 SIGCHLD 信号,进入 sigChildHandler():
- waitpid() 处理子进程状态。
- server.lastsave = server.unixtime; 更新 BGSAVE 完成时间。
- server.rdb_child_pid = -1; 清空 BGSAVE 进程 ID。
- 代码实现
(1) fork() 创建 BGSAVE 进程
在 rdbSaveBackground():
if ((childpid = fork()) == 0) {
/* 这是子进程,执行 rdbSave() */
if (rdbSave(server.rdb_filename) == C_OK) {
exit(0); // 成功,返回 0
} else {
exit(1); // 失败,返回 1
}
}
✅ 子进程成功执行 rdbSave() 后,调用 exit(0) 退出。
(2) SIGCHLD 处理
Redis 在 setupSignalHandlers() 里注册 SIGCHLD 处理函数:
signal(SIGCHLD, sigChildHandler);
SIGCHLD 触发时,调用 sigChildHandler():
void sigChildHandler(int sig) {
pid_t pid;
int stat;
// 处理多个可能结束的子进程
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
if (pid == server.rdb_child_pid) {
// 子进程退出时,检查状态
if (WIFEXITED(stat) && WEXITSTATUS(stat) == 0) {
server.lastsave = server.unixtime; // 记录成功时间
}
server.rdb_child_pid = -1; // 清空 RDB 进程 ID
}
}
}
✅ waitpid(-1, &stat, WNOHANG) 作用:
- -1:等待任何子进程。
- WNOHANG:如果没有子进程退出,不阻塞进程。
- 总结
事件 | 触发者 | 作用 |
---|---|---|
fork() | 父进程 | 创建 BGSAVE 子进程 |
rdbSave() | 子进程 | 执行 RDB 持久化 |
exit(0) | 子进程 | BGSAVE 完成,自动发送 SIGCHLD |
sigChildHandler() | 父进程 | 处理 SIGCHLD,更新 server.lastsave |
✅ SIGCHLD 机制 让 Redis 父进程可以正确追踪 BGSAVE 的状态,而无需主动查询子进程。