如有侵权,请联系~
如有错误,也欢迎批评指正~
本篇文章大部分是来自学习《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 |
通用命令(如 DEL 、EXPIRE 、RENAME 等)。 |
s |
字符串命令(如 SET 、GET 、INCR 等)。 |
l |
列表命令(如 LPUSH 、LPOP 等)。 |
h |
哈希命令(如 HSET 、HGET 等)。 |
z |
有序集合命令(如 ZADD 、ZREM 等)。 |
x |
过期事件(当键因过期而被删除时触发)。 |
e |
驱逐事件(当键因内存不足而被驱逐时触发)。 |
A |
启用所有事件(相当于 g$lshzxe 的组合)。 |
常见组合:
组合 | 含义 |
---|---|
KEA |
启用所有键空间和键事件通知。 |
Ex |
仅启用键事件通知中的过期事件。 |
Kg |
仅启用键空间通知中的通用命令事件。 |
Ksx |
启用键空间通知中的字符串命令和过期事件。 |
通知功能是通过 notifyKeySpaceEvent函数实现的,每个命令执行成功都会执行这个方法。这个方法的底层其实是使用的发布订阅功能。
键空间通知:
频道格式:_keyspace@<db>_:<key>
消息内容:操作类型(如 set、del、expire 等)。
键事件通知:
频道格式:_keyevent@<db>_:<event>
消息内容:键名。
键空间和键事件通知流程图: