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的键。

image-20250413182138128

类似,EXPIREAT或者PEXPIREAT表示在某个UNIX时间戳过期。

TTL命令和PTTL命令则返回键的剩余时间

设置过期时间的实现

都可以转化为PEXPIREAT执行。

### 数据库保存键过期时间

typedef struct redisDb {
// …
  dict* expires; // 过期字典:保存所有键的过期时间。
} redisDb;

过期字典的键是一个指针,指向键空间的某个键。

过期字典的值是一个long long的整数,保存对应的过期时间——毫秒级的unix时间戳。

移除过期时间

PERSIST命令移除键的过期时间。会从过期字典移除。

image-20250413182225724

过期键的判定

通过TTL和PTTL返回

删除策略

删除策略,已知一个键过期,如何删除?

  • 定时删除:对键设置过期时间同时,创建定时器,定时器到了立即执行。
  • 惰性删除:每次从键空间读键时候,过期则删除。
  • 定期删除:每隔一段时间,数据库检查,删除过期键。

定时删除

优点:定时删除能及时释放内存。

缺点:

  1. 如果过期键比较多,同一时间会删除大量键,占用大量CPU时间。
  2. redis创建定时器需要时间时间,时间事件实现方式是无序链表,复杂度0(N),大量事件事件不能高效处理

事实上,如果有大量命令请求需要处理,又不缺少内存,不应该此时去处理过期键,而是优先命令请求。

综述:所以并不现实。

惰性删除

优点:对CPU友好,只在非做不可情况下去做,且可以直接删除当前处理键。

缺点:无用的垃圾数据会占用大量内存。

比如,记录一些log,记录后很少访问,内存大大浪费。

定期删除

定期删除策略为折中,但难度在于设置删除频率。

RDB持久化

RDB 格式

在 Redis RDB 文件中,文件标记(magic values)用于区分数据结构、类型和边界,确保存储和解析一致。选择合适的值通常遵循以下原则:

  1. 唯一性:确保标记值不会与正常数据冲突。(普通数据不会用到的字节)
  2. 可扩展性:预留空间,避免未来变更影响格式
  3. 易解析:选择固定字节长度,避免不必要的位运算

总结

标记 作用 典型取值
文件头 版本识别 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 进程状态能够被正确追踪。

  1. signal 机制在 Redis BGSAVE 的作用

Redis 采用 SIGCHLD 信号 监测 BGSAVE 进程的退出:

  • 子进程 exit(0)(成功)或 exit(1)(失败)后,Linux 自动向父进程发送 SIGCHLD 信号。
  • 父进程通过 SIGCHLD 处理函数 sigChildHandler() 识别 BGSAVE 的结果:
    • 成功:更新 server.lastsave(最后成功 BGSAVE 的时间)。
    • 失败:清理 server.rdb_child_pid,避免误判进程状态。
  1. BGSAVE 触发流程

  2. Redis 主进程 调用 fork() 创建 BGSAVE 子进程:
    • 子进程 执行 rdbSave() 写入 RDB 文件。
    • 父进程 继续接收客户端请求,等待 SIGCHLD。
  3. 子进程执行 rdbSave() 完成后,调用 exit(0) 退出:
    • 操作系统自动向父进程发送 SIGCHLD 信号。
  4. Redis 主进程收到 SIGCHLD 信号,进入 sigChildHandler():
    • waitpid() 处理子进程状态。
    • server.lastsave = server.unixtime; 更新 BGSAVE 完成时间。
    • server.rdb_child_pid = -1; 清空 BGSAVE 进程 ID。
  5. 代码实现

(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:如果没有子进程退出,不阻塞进程。
  1. 总结
事件 触发者 作用
fork() 父进程 创建 BGSAVE 子进程
rdbSave() 子进程 执行 RDB 持久化
exit(0) 子进程 BGSAVE 完成,自动发送 SIGCHLD
sigChildHandler() 父进程 处理 SIGCHLD,更新 server.lastsave

✅ SIGCHLD 机制 让 Redis 父进程可以正确追踪 BGSAVE 的状态,而无需主动查询子进程。

results matching ""

    No results matching ""