Redis的过期删除键策略实现
前面我们已经提到过,删除策略总共有以下三种
- 定时删除
- 惰性删除
- 定期删除
Redis对于后面两种策略都有实现,至于为什么不实现定时策略,前面已经说过,定时器需要使用时间事件,创建时间事件的代价比较大,是不现实的。
Redis惰性删除策略的实现
过期键的惰性删除策略是由expireIfNeeded函数执行的,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查,步骤分别如下
- 如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中进行删除
- 如果输入键未过期,那么expireIfNeeded函数将不做任何动作
进入expireIfNeeded之后
- 如果键过期,键空间中删除该键,并解除过期字典里面的关联,然后再执行读写命令(此时读命令一般都为nil,因为被删除了)
- 如果键未过期,不做任何动作,执行读写命令
用图表示
Redis定期删除策略的实现
过期键的定期删除策略是由activeExpireCycle函数去实现的,Redis会周期性的去执行activeExpireCycle函数,在规定的时间里,分多次去遍历服务器中的数据库(因为函数是有运行时间的,规定时间不能完成就会停止处理),并且从数据库的expires字典(过期字典)中随机检查一部分键的过期时间,并删除其中的过期键。
伪代码形容整个activeExpireCycle函数
//默认每次检查的数据库数量
DEFAULT_DB_NUMBER = 16;
//默认每个数据库需要检查的键数量
DEFAULT_KEY_NUMBER = 20;
//记录检查进度,即检查了数据库的数量
current_db = 0;
def activeExpireCycle(){
//判断服务器的数据库数量是否小于默认检查的数量
IF(server.dbnum < DEFAULT_DB_NUMBER){
//如果小于,就要根据现有的数据库数量进行检查,否则会越界
db_numbers = server.dbnum;
}
//如果不小于,那就按照默认的检查数量去进行
else{
db_numbers = DEFAULT_KEY-NUMBER;
}
//遍历各个数据库
FOR i IN range(db_numbers):
//先判断进度是否已经完成了,即是否检查完数据库了
//如果当前检查进度,即检查过的数据库数量等于服务器拥有的数据库,那就从头开始,进度归零
IF(current_db == server.dbnum){
current_db = 0;
}
//获取当前数据库(全局记录的db)
redisDB = server.db[current_db];
//让current_db自增,为了下一次的检查
current_db++;
//检查指定数量数据库的键
FOR j IN range(DEFAULT_KEY_NUMBER){
//如果里面没有过期键,就跳出
IF(redisDB.expires.size() == 0){break;}
//通过expires字典随机获取一个键
key_with_ttl = redisDB.expires.get_random_key();
//检查键是否过期,如果过期就删除它
IF(is_expired(key_with_ttl)){
delete_key(key_with_ttl);
}
//没过期就不做任何处理
//最后一步判断执行时间是否达到了上限
if(reach_time_limit()){
//达到上限就结束,等下一次的定期删除
return
}
}
}
重点
- 拥有全局变量来记录当前进度(current_db,记录检查过的数据库数量,同时记录第一个未检查过的数据库),还有配置文件上有默认的每次定期删除策略检查的数据库数量和检查每个数据库多少个键(即DEFAULT_DB_NUMBER和DEFAULT_KEY_NUMBER)
- 函数每次运行时,都根据参数去数据库中扫描指定数量的数据库,每个数据库检查指定数量的键值对,并删除里面的过期键
- 如果在检查键值对的时候,函数执行时间达到了规定的时间,就会停止检查,这不会影响下一次的检查,因为current_db记录了下一个未检查的数据库,下一次会从这个数据库开始
- 当current_db等于服务器拥有的数据库数量,就代表所有数据库已经都被检查过了,需要重新开始新一轮的检查工作
AOF、RDB和复制功能对过期键的处理
RDB和AOF是Redis的两种持久化策略,后面再详说,现在简单认识一下这两种持久化策略是怎么处理过期键的
RDB
RDB持久化策略是将数据持久化进磁盘的RDB文件里面。
生成RDB文件
在执行SAVE命令或者BGSAVE命令会创建一个新的RDB文件,程序会对数据库中的键进行检查,如果键已经过期是不会被保存到新建的RDB文件中的。
因此,数据库中包含过期键不会对新生成的RDB文件造成影响(但旧的RDB文件可能会包含过期键,因为那时候键还没有过期,要等下一次覆盖RDB文件的时候才会刷新)。
载入RDB文件
启动Redis服务器时,如果使用的是RDB功能的话,那么服务器将对RDB文件进行载入
载入情况又分为主服务器模式载入和从服务器模式载入
- 如果服务器以主服务器模式运行,那么在载入RDB文件时,程序会对RDB文件里面的键进行检查,如果过期了就不会载入,过期的了就会被忽略掉。
- 如果服务器以从服务器模式运行,那么在载入RDB文件时,程序会加载RDB里面所有的键,不论是否过期,虽然过期键被载入,不过在主从服务器进行同步更新的时候,从服务器的数据库会被清空,替换成主服务器的数据,过期键也在这里被清掉了。
所以,一般来说,过期键对载入RDB文件的从服务器也不会造成影响的
AOF
AOF持久化策略其实记录的是写入的redis命令,记录进AOF文件中,然后加载的时候,其实就是执行这些redis命令
AOF文件写入
当服务器以AOF持久化模式运行时,如果数据库中的某个键过期了,且这个键还没有被定期或者惰性删除,是不会AOF产生影响的。
当这个键被定期或者惰性删除时,AOF文件也会追加一条DEL命令,去显示地标识这个键被删除
举个栗子
- 创建一个5s后过期的键,AOF文件记录了该创建命令
- 5s后使用访问该键,发现过期,会执行惰性删除策略(定期删除策略在这里也会执行删除),把键删除掉
- 同时在AOF文件中追加DEL命令
- 访问放回nil值
AOF文件重写
和生成RDB文件一样,已经过期的键不会被重写入AOF文件中,因此,数据库包含过期键不会对AOF造成影响
复制功能
当服务器运行在复制模式下,从服务器的过期键删除动作由主服务控制
- 当主服务器在删除一个过期键后,会显示地向所有从服务器发送一个DEL命令,通知从服务器去删除该键值对
- 从服务器在执行客户端发送的读命令时,及时读取的是过期键,也不会进行删除,而是像处理未过期的键一样去处理,返回过期键里面对应的值对象
- 从服务器只有在收到主服务器的DEL命令才会去删除键
数据库通知
数据库通知可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况
//客户端在指定频道上发布信息
publish channel message
//客户端订阅频道
subscribe channel
Redis会自动发送上面所示的两种通知
- 第一种为键空间通知
- 第二种为键事件通知
foo指的就是键值对对象的key指,所以keyspce@0__foo其实就是频道名称,del是指对其执行的操作命令,或者keyspace@0:del是频道名称,foo就是其操作的键值对对象key值
不过因为数据库通知是会占用CPU的,所以默认会关闭,要在配置文件进行开启
键空间通知
键空间是数据库里面的属性,当用户在这个数据库里面进行对键值对的操作时候,Redis就会发送通知给keyspace@0频道,所有关注了keyspace@0频道的用户都会接收这个消息
举个栗子
首先将配置文件的notify-keyspace-events的值改为AK,标识开启键空间通知,并且接收所有类型
//关注了0号数据库的message对象
subscribe __@keyspace@0__:message
//开启另一个redis,创建message为键名的字符串对象
set message haha
键事件通知
与键空间通知不同,键空间通知是将对指定键的操作通知给订阅频道的客户端,而键事件通知是指将发送了指定事件的键值对对象的键名发送给订阅频道的客户端
举个栗子
首先将配置文件的notify-keyspace-events的值改为AE,标识开启接收所有键事件通知。
//订阅0号数据库的del事件
subscribe __keyevent@0__:del
//另一个数据库执行Del事件
del msg
notify-keyspace-events的配置
这里简单介绍一下这个配置,详细可以看配置文件的注释
- AKE:服务器发送所有类型的键空间通知和键事件通知
- AK:服务器发送所有类型的键空间通知
- AE:服务器发送所有类型的键事件通知
- K$:只发送和字符串有关的键空间通知
- EI:只发送和列表键有关的键空间通知
发送通知的实现原理
发送数据库通知的功能是由notify.c/notifyKeyspaceEvent函数去执行的
**void notifyKeyspaceEvent(int type,char event,robj key,int dbid);
- type其实就是当前想要发送的通知的类型,程序会根据这个值去判断当前发送通知的类型是否跟配置文件里面的notify-keyspace-event匹配,从而决定通知是否发送
- event是事件的名称,用字符指针来实现字符串,比如删除事件为DEL,过期事件为EXPIRE
- key是产生事件的键对象
- dbid是数据库号
当开启了数据库通知之后,当执行Redis命令之后,命令的实现函数在实现操作成功后都会调用notify-KeyspaceEvent函数,去判断是否需要发送通知
实现的伪代码
def notifyKeyspaceEvent(type,event,key,dbid);
//先判断是否符合配置文件的设定的要求
//如果不符合直接结束
if not(server.notify_keyspace_events & type) return;
//符合条件进行发送通知
//如果配置的要求是键空间,则发送键空间通知
if(server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE)
//构建频道名字
channel = "__keyspace@{dbid}__:{key}".format(dbid=dbid,key=key)
//发送通知
pubsubPublishMessage(chan,event);
End if
//如果配置的要求有键事件,发送键事件通知
if(server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT)
//构建频道名字
channel = "__keyspace@{dbid}__:{event}".format(dbid=dbid,event=event)
//发送通知
pubsubPublishMessage(chan,event)
End notifyKeySpaceEvent
注意
pubsubPublishMessage其实就是PUBLISH命令的实现函数,执行这个函数其实就是去执行PUBLISH命令,后面再说这个命令的实现。
总的来说,发送通知的实现的步骤如下
- 先判断当前事件类型是否符合配置文件里面的notify-keyspace-events
- 如果符合,则判断配置文件里面的notify-keyspace-events是哪种通知
- 两个通知类型都要判断,因为notify-keyspace-events=AKE时,是两种都发送的
- 根据通知的类型去生成channel(频道名字),然后调用publish命令去发送通知