参考:[Redis设计与实现]
文章目录
- 1、数据库 -数据结构与实现
-
- 1、数据结构
- 2、客户端访问与数据库切换
- 3、单个数据库结构实现
- 4、读写键空间触发额外操作
- 2、键过期判定与实现
-
- 1、过期设置命令
- 2、过期时间的结构化存储
- 3、删除策略
-
- 1、常见删除策略对比
-
- 1、定时删除
- 2、惰性删除
- 3、定期删除
- 2、Redis使用的删除策略
-
- 1、惰性删除的实现
- 2、定期删除的实现
- 3、RDB复制对过期键的处理
-
- 1、生成RDB文件
- 2、载入RDB文件
- 4、AOF赋值对过期键的处理
-
- 1、AOF文件写入
- 2、AOF文件重写
- 3、复制
- 4、数据库订阅与通知
-
- 1、功能描述
- 2、功能实现
1、数据库 -数据结构与实现
1、数据结构
Redis服务器所有的数据库都保存在 redis.h/redisServer结构中的db数组中,也就是说对于Redis来说不同的数据库其实是对应的程序中的不同的数组。
db数组的每个项都是 redis.h/redisDb结构,并且redis通过redisServer中的dbNum属性来决定初始化的数据库数量,这个属性是通过database选项进行配置,默认是16.
redisServer结构如下
struct redisServer {
redisDb *db,//服务器数据库的数组
dbNum int, //服务器上数据库数量的配置参数
}
具体的数据库结构关系如下:

2、客户端访问与数据库切换
默认情况下,Redis客户端的目标数据库是0号数据库。客户端可以通过SELECT命令来实现目标数据库切换
在Redis内存,通过redisClient的数据结构来记录客户端当前的目标数据库等信息。主要是通过redisDb的指针来存取当前目标数据库。
redisClient结构如下:
typedef struct redisClient{
redisDb *db //记录客户端当前的目标数据库
//...
}
通过SELECT命令改变目标数据库其实就是修改了,redisDb的指针指向,具体的结构关系如下:
3、单个数据库结构实现
服务器中的每个数据库都是由 redis.h/redisDb的数据结构表示和实现的,而redisDb又是通过dict数据字典来保存了数据库中的所有键值对,这个字典就是所谓的键空间。
redisDb数据结构:
typedef struct redisDb{
//...
dict *dict,//数据库键空间,保存数据库中的所有键值对信息
}
键空间的特性:
- 键空间的键就是数据库的键,每一个键都是一个字符串对象(不可变和唯一性)
- 键空间的值就是数据库的值,值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的一种
具体看一下数据库键空间的例子,对键空间的增删改查与常规数据库类似,略过不表:
4、读写键空间触发额外操作
redis对键空间进行读取,会触发额外的维护操作,包括如下:
- 读写之后,服务器会根据键是否存在来更新服务器的键空间命中次数或者键空间的不命中次数。
- 读写之后,更新键的LRU时间,这个属性可用于计算键的空闲时间。
- 如果数据库读取键的时候发现键已经过期,会先删除这个键,然后才会执行余下操作。
- 如果使用WATCH命令监视某个键,服务器会标记被修改后的键为dirty。
- 服务器每次修改一个键都会对脏键计数器的值加一,这个计数器会触发服务器的持久化以及复制操作。
- 如果开启了数据库通知功能,则在修改之后会触发相应的数据库通知。
2、键过期判定与实现
1、过期设置命令
- EXPIRE :将key的生存时间设置为ttl 秒
- PEXPIRE :将key的生存时间设置为ttl 毫秒
- EXPIREAT :将过期时间设置为timestamp所指定的秒数时间戳
- PEXPIREAT :将过期时间设置为timestamp所指定的毫秒数时间戳
- TTL :计算key剩余的生存时间秒数
- PTTL :计算key剩余的生存时间毫秒数
- PERSIST :移除key对应的过期时间
2、过期时间的结构化存储
redis通过redisDb结构的expires字典保存数据库所有键的过期时间
- 过期字典的键是一个指针(指向对应的某个键对象)
- 过期字典的值是一个long long 类型的正数,保存了对应数据库键的过期时间(毫秒精度的UNIX时间戳)
expire字典结构:
typedef struct redisDb{
dict *expires;//过期数据字典,保存键的过期时间
}
结构化关系:
3、删除策略
1、常见删除策略对比
1、定时删除
优点:
- 这种策略对内存是最友好的,通过定时器实现,定时删除可以保证过期键尽快删除,并释放过期键所占用的内存空间
缺点:
- 对于CPU时间(CPU轮转执行)是最不友好的,在过期键比较多的情况下,删除过期键会占用相当一部分CPU时间,在CPU时间紧张的情况下,CPU时间被用在删除与当前任务无关的过期键上,对服务器的响应时间和吞吐量会造成影响
- 创建一个定时任务需要用到redis服务器中的时间事件,当前的时间事件的实现方式是无序链表,查找的复杂度为O(N),并不能高效的处理大量的时间事件。
2、惰性删除
优点:
- 对CPU时间是最友好的,程序只会对需要被取出的键进行是否过期的判断,没有在无关的过期键上面花费任何CPU时间
缺点:
- 对内存最不友好,如果一个键过期了,但是没有被调用,那么它会一直停留在内存,占用的内存空间不会被释放。服务器不会自动去释放它们,对于Redis这种运行非常依赖内存的服务器来说,非常不友好。
3、定期删除
定期删除是前两种方式的整合和折中。采用定期删除必须根据情况设置删除操作的执行时长和频率,不然就可能退化成上面两种其中任意一种删除策略,缺点暴露
2、Redis使用的删除策略
Redis的实现采用 惰性删除和定期删除两种删除策略。配合这两种删除策略,服务器可以很好的在合理使用CPU时间和避免内存浪费之间取得平衡
1、惰性删除的实现
惰性删除策略是通过 db.c/expireIfNeeded函数实现,所有读写数据库的命令在执行之前都会先调用这个方法进行检查。如果键过期则进行删除,否则不做具体操作。
具体的实现分为两种情况:1、键不存在(前一次操作被删除)2、键存在。具体的操作过程如下(以GET举例):
2、定期删除的实现
定期删除通过redi.c/activeExpireCycle函数实现,当redis服务器周期性操作redis.h/serverCron函数执行的时候会调用定期删除函数,它会在规定时间内,分多次遍历服务器的各个数据库,从expire字典中随机检查一部分数量键的过期时间,删除其中的过期键。
对应的函数属性信息:
{
DEFAULT_DB_NUMBERS = 16 //默认每次需要检查的数据库数量
DEFAULT_KEY_NUMBERS = 20 //默认每次检查的key的数量
current_db = 1 //全局变量,检查进度,因为是分多次进行检查,所以记录的是当前执行的数据库,下一次操作就是从下一个数据库开始,检查结束,变量直接置为0
}
操作过程:
- 根据要检查数据库数量,进行遍历,取出检查的key数量个键,进行判断expire字典过期时间,如果过期则进行删除
- 本次检查保存检查进度信息,等待本次的下一波检查开始,从检查进度对应的下一个数据开始检查,执行上一步相应操作
- 一次检查分为几轮,本次检查结束之后,把检查进度信息置为0,等待下一次被触发。
3、RDB复制对过期键的处理
1、生成RDB文件
在执行SAVE或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键bu8hui保存到新创建的RDB文件中。
因此,数据库中包含过期键不会对生成新的RDB文件造成影响。
2、载入RDB文件
在启动Redis服务器时,如果开启了RDB功能,服务器对RDB文件进行载入有两种情况:
- 如果以主服务器模式运行,载入RDB文件时,程序会对键进行检查,未过期的键会被载入到数据库,过期的会被忽略
- 如果是以从服务器模式运行,载入RDB文件时,程序会把所有键都进行载入,当主服务器进行数据同步的时候,从服务器的数据库会被清空,所以过期键对载入RDB文件不会造成影响。
4、AOF赋值对过期键的处理
1、AOF文件写入
当程序以AOF持久化模式运行时,如果某个键已经过期,但是还没有被删除策略删除,对AOF文件不会造成任何影响。因为AOF文件写入对于过期的key是这么操作的:
- 从数据库中删除过期键
- 追加一条 DEL 命令到AOF文件
- 向执行读取的命令返回空回复
2、AOF文件重写
和AOF文件写入类型,不会对过期的key进行写入,所以过期的key对AOF文件重写不会造成任何影响。
3、复制
当服务器运行在复制模式下,从服务器的过期删除动作主要由主服务器控制
- 主服务器删除一个过期键之后,会显示的想所有从服务器发送一个DEL命令,告诉服务器删除这个过期键
- 从服务在执行客户端的命令,即使碰到过期键也不会对其进行删除,而是想处理未过期键一样(通过主服务器统一删除过期键,可以保证主从服务器的一致性)
- 从服务器收到DEL命令之后,才会删除过期键
4、数据库订阅与通知
1、功能描述
该功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
Redis客户端订阅命令:
SUBSCRIBE _ _keyspace@【数据库编号】_ _ :【命令或者key名】
,比如说订阅0号数据库的message这个key的命令就是:
SUBSCRIBE _ [email protected]_ _:message
服务器通过配置**
notify-keyspace-events**
选项决定发送通知的类型:
2、功能实现
发送通知功能由notify.c/notifyKeysSpaceEvent函数实现:
/**
* type:当前想要发送的通知类型
* event:时间的名称
* keys:产生事件的键
* dbid:数据库号码
*/
void notifyKeysSpaceEvent(int type,char *Event,robj *key,int dbid)
当一个redis命令需要发送数据库通知的时候会调用该函数,实际实现步骤:
- 判断操作类型是否是notify-keyspace-events配置的允许发送的内容,如果不是则直接返回
- 判断服务器是否允许发送键空间通知,如果允许则会发送事件通知
订阅事件其实本质就是通过PUBLISH命令来执行的