Redis09:过期时间与删除策略、redis中的特殊线程

服务器中的数据库

redis的数据都保存在服务器的redisServer结构中,它其中有一个redisDb类型的属性*db,它保存着服务器中所有的数据库,初始化服务器时,程序会根据redisServer结构中的int类型属性dbnum来决定创建多少个数据库,它的默认值是16,所以db会指向一个大小为16的数组,每个数组都是一个数据库,数据库的类型是redisDb。

struct redisServer{
	//数据库数组
	redisDb *db;
	//数据库数量
	int dbnum;
	...
}

默认情况下redis客户端操作的是0号数据库,客户端可以通过select命令来切换数据库,如:

select 2

redis客户端中的关键结构是redisClient,它有一个指向redisDb的指针*db,表示当前客户端正在使用的数据库,select命令的本质就是调整该指针的指向。

切换完成后客户端会在输入符旁边提示当前所使用的目标数据库,当使用其他语言来操控redis时要注意切换数据库的问题,因为此时并没有任何提示信息来指示当前使用的是什么数据库,甚至也没有直接返回数据库号的命令,所以当执行重大命令时,如flushdb,最好先执行一个select命令,保证命令执行在正确的数据库。

redisDb中有一个dict类型的属性*dict,它保存了该数据库中的所有键值对,这个字典被称为键空间key space。

在执行命令时我们不仅需要读取和修改键空间的数据,还要对键空间进行额外的维护工作,如更新LRU时间、懒惰删除、记录脏数据信息、引发通知功能等。

生存时间和过期时间

相关命令

我们可以使用expire命令来设置键的生存时间:

expire key 5

设置完毕后5秒该键就会过期。pexpire和这个命令类似,但是是用毫秒作为单位的。

expireat命令可以给键以秒时间戳的方式设置一个过期时间:

expireat key 1377257300

pexpireat和这个命令类似,但是它是用毫秒时间戳为单位的。

expire、pexpire、expireat和pexpireat最终在底层都会执行pexpireat命令。

ttl命令和pttl命令可以返回一个键的剩余生存时间,分别以秒和毫秒位单位。

persist命令可以移除一个键的过期时间。

过期字典

redisDb结构的expires字典保存了数据库中所有键的过期时间,这个字典被称为过期字典。

过期字典的哈希节点有键和值,键是一个指针,指向键空间中的某个键对象,值就是一个long long类型的整数,这个整数保存了这个键对象的过期时间,它是一个毫秒精度的时间戳。

以上的命令都是通过检查过期字典的值来评价键对象是否过期的,persist命令可以在过期字典中查找给定的键,并且解除键和值在过期字典中的关联。

删除策略

在redis中一个键过期了通常不会立即删除,因为立即执行删除虽然可以尽快的释放内存,但是同一时间过期键很多的时候,会对CPU产生很大的压力,影响服务器的正常业务。redis采用定期删除和惰性删除相结合的方式来完成过期键的删除。

惰性删除的意思就是程序只有在取出键的时候才会对其进行过期检查,不会在无关的过期键上浪费时间,它非常节约CPU资源,但是容易导致运行一段时间后无内存可用,因此我们需要定期删除来辅助。

定期删除每隔一段时间就会执行一次删除过期键操作,通过控制删除的频率和执行时长来尽量避免对CPU的影响。

惰性删除的实现

在读写数据库的redis命令执行前都会调用懒惰删除的函数,如果键已经过期就会将其从数据库中删除,如果还未过期就不做处理。它就像一个过滤器,在命令真正执行之前,过滤掉过期的输入键。对应键是否存在,不同的命令也有不同的处理方式,调用懒惰删除expireIfNeeded函数的过程如下:

定期删除的实现

每当redis的服务器周期性操作serverCron函数执行时,就会调用定期删除的函数。

定期删除会在规定的时间内分多次遍历服务器中的各个数据库,从过期字典中默认抽查20个键,删除其中过期的键。

数据库通知

获取0号数据库中对message键执行的所有命令:

subscribe _ _keyspace@0_ _:message

执行后会持续监控对这个键执行的所有命令,第一个通常是订阅信息,第二个信息开始就是其他命令了。

这种关注某个键执行了什么命令的通知被称为键空间通知,还有一种通知关注某个命令被什么键执行了,这类通知被称为键事件通知,如获取0号数据库中所有执行del命令的键:

subscribe _ _keyevent@0_ _:del

结果中第一个通常是订阅信息,第二个信息开始就是执行该命令的键名了。

服务器可以配置发送什么类型的通知,以及监控的通知的键的类型等。

通知的实现是通过在各种命令的执行过程中进行判断,如果满足通知要求就调用通知函数。

总结

过期策略

为key设置过期时间:expire key 5(5s过期)

查看key的剩余时间:ttl key(未设置超时返回-1,已超时返回-2)

redis的过期实现通过两种方式,分别是懒惰删除和定时遍历。

懒惰删除就是用到某个key的时候再去查它是否过期,如果过期就删除。

定时遍历是一个集中处理的方式,redis会把所有设置了过期时间的key放到同一个字典中,默认每秒10次过期扫描,每次随机查字典中的20个key,然后删除,如果其中过期的比例超过四分之一就重复这个动作,为了防止循环过度规定扫描时间的上限是25ms。不过即使如此,客户端如果设置超时的话,连接失败就会报出大量异常。

redis只能单线程处理,为了避免不产生大批key同时过期卡死,要给过期时间设置一个随机范围,一般用固定活动过期时间+随机冗余时间来防止。

从库的过期策略是被动的,只能等待主库删除key时将操作写进AOF日志文件,然后同步到从库。这种异步同步可能带来不一致的风险。

淘汰策略

可以设置maxmemory来限制最大使用内存,如果超出了还可以选择几种策略来应对:

noeviction:不会继续服务写请求,读请求可以继续进行,这是默认的淘汰策略

volatile-lru:淘汰已经过期的,最少使用的key

volatile-ttl:淘汰已经过期的,剩余寿命最短的

volatile-random:随机淘汰一些已经过期的

allkeys-lru:淘汰那些最少使用的key

allkeys-random:随机淘汰一些key

如果redis只起缓存功能,那么用allkeys就可以,如果想使用持久化功能,那么就必须使用volatile,保证永久的key不被淘汰。

在4.0版本中淘汰策略增加了两项:

volatile-lfu和allkeys-lfu分别是对已经过期的key和所有key进行LFU淘汰,开启了这个选项之后还能获得对象的LFU计数。LFU是根据访问频率来计算热度,优先淘汰热度低的。redis的LFU热度是分钟级的,redis缓存了系统时间戳(因为获取属于系统调用很费时间)。

淘汰的算法

redis采用的是一种近似LRU算法,因为严格的需要消耗大量内存,数据结构很复杂。这种近似LRU为每一个key增加了一个最后一次被访问的时间戳信息,如果发现内存超过maxmemory就会执行LRU,随机选择一些key,然后淘汰掉最旧的key,然后循环进行,直到内存低于maxmemory为止。在redis3.0中还增加了淘汰池来提升这个算法的效果,每次循环出来的key都会放入淘汰池中,然后再选择最旧的key淘汰,剩余保留在淘汰池中。

懒惰删除

删除key的指令是del,如果key非常大,这个指令就会卡顿,此时可以用unlink,它会将删除丢给后台进程异步回收内存,在执行的一瞬间,其他客户端就无法访问到该key了。(如果key很小就会立刻删除)

删除任务会塞进异步任务队列(主线程操作这个队列时会有加锁和解锁操作,如果异步线程在休眠的话还会唤醒),后台线程会依次处理这些任务。

异步清空数据库

flushdb和flushall命令用来清空数据库,但是也很耗时,可以用async来代替异步删除。

AOF刷磁盘

在进行AOF时,将内存中的数据刷新到磁盘的sync函数也是很耗时的,会导致主线程效率下降,所以redis也会使用一个异步线程来操作这个过程,它也有一个属于自己的任务队列。

其他异步操作

还有很多的删除:淘汰时、过期时、rename时这些都可以通过配置来异步完成。

异步删除方案在最初设计时并没有考虑到,是后来才改造的,因为redis的对象共享机制,导致懒惰删除不干净,为了支持懒惰删除,redis将这种机制抛弃,改用了无共享设计。

其他异步操作:主从同步的增量同步

posted @ 2019-09-15 14:21  勇闯8  阅读(711)  评论(0编辑  收藏  举报