Redis服务器将所有数据库都保存在服务器状态redisServer结构的db数组中,db数组的每个项都是一个redisDb结构,每个redisDb结构代表一个数据库。在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库,默认为16个
struct redisServer {
...
// 一个数组保存服务器所有的数据库
redisDb *db;
// 服务器的数据库的数量
int dbnum; /* Total number of configured DBs */
...
}
每个Redis客户端都有自己的目标数据库,每个客户端执行数据库写命令的时候或者读命令的时候,目标数据库就会成为这些命令的操作对象
默认情况下,客户端在0号数据库设置,可以使用SELECT 切换数据库,如下
在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的的目标数据库,这个属性是指向redisDb结构的指针
typedef struct redisClient {
. . .
// 记录客户端正在使用的数据库
redisDb *db;
. . .
}
注意:
目前为止,Redis仍然没有可以返回客户端目标数据库的命令,但是如果使用其他语言操作Redis,并且该语言的客户端没有显示数据库,那么经过数次数据库切换可能会忘记在哪个数据库,特别是执行FLUSHDB这一类的操作之前,一定要先执行SELECT 操作。
Redis是一个键值对(key-value pair)数据库服务器,服务器中的每个数据库都由一个redisDb结构表示,其中,redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间(key space)
键空间和用户所见的数据库是直接对应的
例如执行操作
会形成如下数据结构
当使用Redis命令对数据库进行读写时,服务器不仅会对键空间执行指定读写操作,还会执行一些额外的维护操作
通过 EXPIRE 命令或者 PEXPIRE 命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL),在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0的键
通过 EXPIREAT 和 PEXPIREAT 命令,可以设定一个时间戳,该过期时间是一个UNIX时间戳,当键的过期时间来临时,服务器就会自动从数据库中删除这个键,可以通过TIME命令查看UNIX的时间
TTL命令和PTTL命令接受一个带有生存时间或者过期时间的键,返回这个键的剩余生存时间,也就是,返回距离这个键被服务器自动删除还有多长时间
通过上面的例子,可以看出Redis有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除)
虽然有多种不同单位和不同形式的设置命令,但实际上EXPIRE、PEXPIRE、EXPIREAT三个命令都是使用PEXPIREAT命令来实现的
redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典
typedef struct redisDb {
...
// 过期字典,用于存放有过期时间的对象
dict *expires; /* Timeout of keys with a timeout set */
...
} redisDb;
移除过期时间
PERSIST命令可以移除一个键的过期时间
PERSIST命令就是PEXPIREAT命令的反操作:PERSIST命令在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联
计算并返回剩余生存时间
TTL命令以秒为单位返回键的剩余生存时间,而PTTL命令则以毫秒为单位返回键的剩余生存时间。这两个命令都是通过计算键的过期时间和当前时间之间的差来实现的
过期键的判定
通过过期字典,程序可以用以下步骤检查一个给定键是否过期
通过上面的知识,我们知道了数据库键的过期时间都保存在过期字典中,又知道了如何根据过期时间去判断一个键是否过期,现在剩下的问题是:如果一个键过期了,那么它什么时候会被删除呢?
有以下三种删除策略
定时删除
惰性删除
惰性删除的优缺点和定时删除恰恰相反
定期删除
定期删除是一种较为综合的删除策略,能够兼顾CPU与内存
Redis服务器实际使用的是惰性删除和定期删除两种策略,通过配合使用这两个删除策略,可以很好地合理使用CPU时间和避免浪费内存空间之间取得平衡
过期键的惰性删除策略由expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查(该函数就像一个AOP的前置通知,在真正执行命令之前删除过期的键)
过期键的定期删除策略由activeExpireCycle函数实现,每当Redis的服务器周期性操作serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键
activeExpireCycle函数的工作模式可以总结如下
因为只使用以上过期删除的策略,仍有可能因为有大量的Key没有被删除而导致OOM,所以需要引入过期淘汰策略,在内存使用量超出的时候删除键值对。
LRU 的实现:
Redis 内部维护了一个24位时钟,每个对象也维护了一个时钟,每次使用对象的时候,该时钟就会变成当前系统的时间戳,当需要进行LRU的时候,就选择时钟举例当前系统时间最长的对象淘汰。
LFU 的实现
将24位的时钟分成两部分,前16位代表时间,后8位代表使用的次数,如果以前使用的次数很多,而最近一直不使用会根据时间进行衰减,另外,为了防止某个key刚加入就被删除,所以会对新加入的key赋上一个值初始值5。
Redis的发布与订阅功能由 PUBLISH、SUBSCRIBE、PSUBSCRIBE 等命令组成。
当一个客户端执行SUBSCRIBE命令订阅某个或某些频道的时候,这个客户端与被订阅频道之间就建立起了一种订阅关系。
Redis 将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 字典里面,这个字典的键是某个被订阅的频道,值是一个链表,链表里面记录了所有订阅这个频道的客户端。
struct redisServer {
// ...
// 保存所有频道的订阅关系
dict *pubsub_channels;
// ...
};
所有模式的订阅存放在一个链表中。
struct redisServer {
// ...
// 保存所有模式订阅关系
list *pubsub_patterns;
// ...
};
链表中每个节点又记录了订阅模式和订阅该模式的客户端。
typedef struct pubsubPattern {
// 订阅模式的客户端
redisClient *client;
// 被订阅的模式
robj *pattern;
} pubsubPattern;
当 Redis 客户端执行如下指令之后,会向该频道以及对应匹配的模式发送消息。
PUBLISH <channel> <message>
参考《Redis 的设计与实现》