持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第27天,点击查看活动详情
数据库如何保存键的生存时间和过期时间,以及服务器如何自动删除那些带有生存时间和过期时间的键
生存时间和过期时间有四种,分别是expire、pexpire、expireat、pexpireat。但是他们都是使用pexpireat命令来实现的。其他三个都可以转换为pexpireat
过期时间设置:
set key value
expire key 5000
保存过期时间
redisDb结构的expires字典保存了数据库中所有键的过期时间。键是指针,指向一个long类型的整数(unix时间戳)。
当客户端执行过期时间命令的时候,服务器会在数据库的过期字典中关联给定的数据库键和过期时间
例如:
expire message 1391234400000
过期字典树就变成了这样
expireat原理:
- 如果给定的键不存在于键空间,那么不能设置过期时间
- 存在键空间,在过期字典中关联键和过期时间
- 过期时间设置成功,返回1
移除过期时间
使用persist命令在过期字典中查找给定的键,并解除键和值在过期字典中的关联。
persist原理
- 如果键不存在,或者键没有设置过期时间,那么直接返回
- 如果键存在,那么移除过期字典中给定键的键值对关联
- 键的过期时间移除成功
计算并返回剩余生存时间:ttl和pttl命令都是通过计算键的过期时间和当前时间之间的差。毫秒为单位
过期键的判定
原理:
- 获取键的过期时间
- 如果键没有设置过期时间,那么直接返回false
- 获取当前时间的unix时间戳
- 检查当前时间是否大于键的过期时间
- 注意:其实也可以使用ttl和pttl命令,返回值大于等于0就没过期,但是实际上直接访问字典比执行一个命令快,所以使用访问字典
过期键删除策略(什么时候删除)
- 定时删除:创建一个定时器,定时器在键的过期时间来临时,立即执行对键的删除策略
对内存最友好,因为该策略能保证过期键尽可能快地被删除,从而释放内存,但缺点是对CPU时间不友好,如果过期键比较多,删除过期键的行为会占用相当一部分CPU时间,从而对服务器的响应时间和吞吐量造成影响。 并且创建一个定时器需要用到时间事件,而当前时间事件的实现方式是无序链表,查找一个事件的时间复杂度为O(N)无法高效处理大量时间事件
- 惰性删除:每次从键空间中获取键的时候检查键是否过期,过期则删除,不过期就返回键
对CPU时间最友好,只会在取出键的时候才对键进行过期检查,只删除当前的键,不删除无关过期键,所以不花费多余的CPU时间。但是如果没有被访问的过期键永远不会被删除,也就是内存泄漏
-
定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。并且通过限制删除键的时长和频率来减少删除操作对CPU时间的影响。每秒扫描10次(扫描时间不超过25毫秒),过期扫描,过期扫描不会遍历过期字典的所有key,而是采用一种贪心策略:
- 从过期字典中随机20个key
- 删除这20个key中已经过期的key
- 如果过期的key比率超过1/4,重复步骤1
是前面两种策略的折中方案,难点是确定删除操作执行的时长和频率
Redis采用的是惰性删除和定期删除
惰性删除的实现:
通过db.c/expireIfNeeded函数实现,对输入键进行检查,如果输入键过期,那么expireIfNeeded函数将输入键从数据库中删除,如果未过期就不动作。这里面分为两种情况,键存在或者不存在,不存在直接返回空
定期删除策略的实现:
redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性地操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,在规范的时间内多次遍历服务器的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并且删除其中的过期键
原理:
默认每次检查数据库的数量:16
默认每个数据库检查的键数量:20
全局变量current_db=0用来记录检查进度
遍历各个数据库,如果current_db的值等于服务器的数据库数量,说明已经遍历了服务器的所有数据库一次,重置为0,开始新一轮遍历
遍历的步骤:
-
获取当前要处理的数据库
-
数据库索引增1,指向下一个要处理的数据库
-
检查数据库键
- 如果数据库中没有一个键带有过期时间,直接跳过
- 如果有键带有过期时间,随机获取一个带有过期时间的键
- 检查键是否过期,如果过期就删除调
- 如果已经到达时间上限了,就停止处理
\