Redis设计与实现-服务器中的数据库

发布于:2025-03-12 ⋅ 阅读:(8) ⋅ 点赞:(0)

如有侵权,请联系~
如有错误,也欢迎批评指正~
本篇文章大部分是来自学习《Redis设计与实现》的笔记

1、 redis数据库

Redis默认有16个数据库,可以根据配置设置数据库的数量。数据库存储在数据库数组中,如下redisServer定义中的*db字段。

redisServer 是 Redis 服务器的核心数据结构,它包含了 Redis 运行时的所有状态和配置信息。redisServer 是一个全局变量(通常命名为 server),在 Redis 源码中定义为 struct redisServer 类型。它是 Redis 内部管理数据库、客户端连接、命令执行等核心功能的基础。

struct redisServer {
    // 配置相关
    char *configfile;          // 配置文件路径
    int port;                  // 监听端口
    char **bindaddr;           // 绑定地址
    int databases;             // 数据库数量

    // 数据库相关
    redisDb *db;               // 数据库数组
    int dbnum;                 // 当前数据库索引

    // 客户端连接相关
    list *clients;             // 客户端列表
    list *clients_pending_write; // 等待写入的客户端

    // 命令相关
    dict *commands;            // 命令字典

    // 持久化相关
    char *rdb_filename;        // RDB 文件路径
    int aof_state;             // AOF 状态
    sds aof_buf;               // AOF 缓冲区

    // 事件循环
    aeEventLoop *el;           // 事件循环
    int hz;                    // 定时任务频率

    // 复制与集群
    char *masterhost;          // 主服务器地址
    int repl_state;            // 复制状态
    int cluster_enabled;       // 是否启用集群

    // 统计与监控
    long long stat_numcommands; // 命令总数
    long long stat_numconnections; // 连接总数
};

可以通过select命令进行切换数据库。例如select 2表示切换到2号数据库中。

Redis服务端利用client用来管理每个连接到服务器的客户端的状态和行为的核心数据结构。当一个客户端(如 Redis CLI、应用程序或其他 Redis 客户端)通过网络连接到 Redis 服务端时,Redis 服务端会为该连接创建一个 redisClient 实例。这个实例用于跟踪客户端的状态(如连接信息、执行的命令、数据库选择等),并负责处理客户端发送的请求以及向客户端返回响应。

typedef struct client {
    // 客户端基本信息
    uint64_t id;               // 客户端 ID
    int fd;                    // 文件描述符
    sds name;                  // 客户端名称
    int flags;                 // 客户端状态标志

    // 输入缓冲区
    sds querybuf;              // 输入缓冲区
    int argc;                  // 参数个数
    robj **argv;               // 参数数组
    struct redisCommand *cmd;  // 当前命令

    // 输出缓冲区
    char buf[PROTO_REPLY_CHUNK_BYTES]; // 固定大小的输出缓冲区
    list *reply;               // 动态分配的输出缓冲区

    // 数据库相关
    redisDb *db;               // 当前数据库
    int dictid;                // 数据库索引

    // 复制相关
    int replstate;             // 复制状态
    long long reploff;         // 复制偏移量

    // 阻塞与超时
    mstime_t bpop_timeout;     // 阻塞超时时间
    long long lastinteraction; // 上次交互时间

    // 统计信息
    long long query_start_time; // 命令开始执行时间
    size_t obuf_mem;           // 输出缓冲区内存占用

    // 其他
    int authenticated;         // 是否已认证
    connection *conn;          // 连接对象
    dict *pubsub_channels;     // 订阅的频道
    list *pubsub_patterns;     // 订阅的模式
} client;

client中的db字段就表示当前使用的数据库。select命令其实底层实现就是修改client的db这个字段,这个字段就会指向redisServer db数组的具体某个数据库。

2、 数据库键空间

redis默认有16个数据库,每个数据库都是键值对数据库,每个数据库都是有redisServer中提到的redisDB数据结构存储。

typedef struct redisDb {
    // 键空间,保存所有的键值对
    dict *dict;               // 主字典,存储键值对
    dict *expires;            // 过期字典,存储键的过期时间。过期key的删除策略:定期删除和惰性删除

    // 阻塞操作
    dict *blocking_keys;      // 阻塞键的集合
    dict *ready_keys;         // 已经准备好的阻塞键

    // 订阅发布
    dict *pubsub_channels;    // 频道订阅关系
    list *pubsub_patterns;    // 模式订阅关系

    // 持久化相关
    int id;                   // 数据库 ID
    long long avg_ttl;        // 平均 TTL

    // 事务相关
    dict *watched_keys;       // 被监视的键

    // Redis Cluster 相关
    unsigned char slots[CLUSTER_SLOTS / 8]; // 哈希槽位图
    dict *slots_to_keys;      // 哈希槽到键的映射
} redisDb;

这里的键空间就是和我们所见的数据库直接对应。存储的键就是dict中的键,存储的值【字符串、列表、哈希…】就是dict中的值。

3、 过期时间和删除策略

和键过期相关的命令:

命令 描述 示例
EXPIRE 设置键的过期时间(秒) EXPIRE key seconds
PEXPIRE 设置键的过期时间(毫秒) PEXPIRE key milliseconds
EXPIREAT 设置键在指定时间戳(秒)后过期 EXPIREAT key timestamp
PEXPIREAT 设置键在指定时间戳(毫秒)后过期 PEXPIREAT key milliseconds-timestamp
TTL 查看键剩余的过期时间(秒) TTL key
PTTL 查看键剩余的过期时间(毫秒) PTTL key
PERSIST 移除键的过期时间,使其永不过期 PERSIST key
SET … EX 设置键值对并指定过期时间(秒) SET key value EX seconds
SET … PX 设置键值对并指定过期时间(毫秒) SET key value PX milliseconds

无论是EXPIRE、PEXPIRE还是EXPIREAT底层都是使用PEXPIREAT命令进行实现的。

底层实现:
通过上面redisDB数据结构就可以看到有个字典属性expires过期字典,所有的键值对都存储到键空间dict上,而所有键的过期时间都存储到过期字典expires中。expires中的key就是指向键空间中的键对象key,而value就是一个long long类型的整数。

如何判断一个键是不是过期了呢?
通过过期字典中某个键的过期时间【键的value值】,如果当前时间大于过期时间证明这个键已经过期。

键过期了,怎么删除呢?
删除策略主要有三种:

  • 定时删除:每当给某个键设置过期时间的时候,都会为其创建一个定时器。当到过期时间之后,就会立刻删除。所以这种方式对于内存是友好的,一旦过期就会删除不会占用多余的内存;缺点:浪费CPU资源,尤其是存在大量的过期键,定时器,CPU的浪费更明显。
  • 惰性删除:只有用到这个键的时候才会校验这个键值对是不是已经过期了,如果过期则进行删除。这种方式不会浪费占用多余CPU资源,但是会占用大量内存。例如某些键已经过期,但是长期又不访问就导致一直占用内存,可以看成内存泄漏。Redis底层通过expireIfNeed方法进行实现。
  • 定期删除:这种策略是定期的去清理一定数量的过期键。综合衡量CPU和内存资源,但是这种方式的难点是执行的频率和时间。Redis底层通过activeExpireCycle方法进行实现。

Redis使用惰性删除和定期删除相结合的方式进行过期键的删除。

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

类型 描述
生成RDB文件 无论是执行save命令、bgsave命令,还是配置项中的save周期,在进行生成RDB文件的时候,过期键都不会保存,会对数据库中的键进行检查
加载RDB文件 针对于主服务器:在加载RDB文件的时候,会主动过滤掉过期的键值对;从服务器则会将所有的键都加载到数据库内存中,不过等主从同步的时候,从服务器就会删除自身所有数据,加载主服务器的RDB数据
AOF文件写入 如果数据库中的键过期,不会对AOF文件有任何影响。除非触发定期删除或者惰性删除,在定期删除或者惰性删除的时候,会往AOF文件中写一条del命令
AOF重写 AOF重写和生成RDB文件一样,不会对过期键进行保存
复制 主服务器删除一个过期键之后,会显式地向所有的从服务器发送一个DEL命令;从服务器并不会主动的删除过期键,只有收到主服务器的DEL命令才会删除,这么做为了主从一致性。如果一个键过期了,从服务器没有收到主服务器的DEL命令,那么从服务器仍然存在这个键值对,这个时候当从服务收到客户端读get命令,从服务器会将这个键对应的值返回出去。【如果Redis集群配置读写分离,需要注意这点

5、 通知

这里的通知与发布订阅【客户端发布到频道一个值,服务端就会向订阅这个频道的客户端发送这个值,即订阅者就会收到这个值(客户端和服务端长连接)】不同。
通知分为键空间通知【某个键执行了哪些命令】和键事件通知【某个命令被哪些键执行了】。用于控制通知的参数:notify-keyspace-events
notify-keyspace-events的所有字符取值:

字符 含义
K 启用键空间通知(Keyspace notifications)。
E 启用键事件通知(Keyevent notifications)。
g 通用命令(如 DELEXPIRERENAME 等)。
s 字符串命令(如 SETGETINCR 等)。
l 列表命令(如 LPUSHLPOP 等)。
h 哈希命令(如 HSETHGET 等)。
z 有序集合命令(如 ZADDZREM 等)。
x 过期事件(当键因过期而被删除时触发)。
e 驱逐事件(当键因内存不足而被驱逐时触发)。
A 启用所有事件(相当于 g$lshzxe 的组合)。

常见组合:

组合 含义
KEA 启用所有键空间和键事件通知。
Ex 仅启用键事件通知中的过期事件。
Kg 仅启用键空间通知中的通用命令事件。
Ksx 启用键空间通知中的字符串命令和过期事件。

通知功能是通过 notifyKeySpaceEvent函数实现的,每个命令执行成功都会执行这个方法。这个方法的底层其实是使用的发布订阅功能。
键空间通知:
频道格式:_keyspace@<db>_:<key>
消息内容:操作类型(如 set、del、expire 等)。
键事件通知:
频道格式:_keyevent@<db>_:<event>
消息内容:键名。

键空间和键事件通知流程图:
在这里插入图片描述