大家好,我是我心永恒的老李。
今天下午的时候,我看到来自于大洋彼岸的短视频:一些擦腚纸贩子都在囤积居奇,高价兜售擦腚纸,看到这些消息真的是雏菊一紧。沙雕肺炎病毒全球肆虐的时候,难道不应该是买口罩保护上面的口么,怎么抢着买擦腚纸招呼下边那个眼了?算了,管不了那么多了,我也囤点儿擦腚纸得了。
买擦腚纸这件小事儿,在这个物欲横流而又穷人穷横的年代,必须要货比三家:「匋玉」和「并夕夕」横向比了下还是「并夕夕」以绝对的优势胜出了!看着「匋玉」上昂贵的擦腚纸价格,我心如刀绞,这公司啥时候堕落成这样了?你看人家「并夕夕」,包邮到家还仅售一块三...
大概三年多以前吧好像是,我曾经有缘去「匋玉」所属的母公司「四十大盗」集团望京总部面试过,大概夭折在了第三轮:
主要不是我差劲,而是竞争对手们太厉害,还因为那条狗子
那天我白嫖了一辆锁已经被暴力开拔的免费ofo到了「四十大盗」望京中心后,刚把ofo横着放倒藏到草丛里,就看到一条狗子疯狂地冲我跑了过来,旁边还有个惊慌失措的小姐姐。当时我心里一横就一个想法:这沙雕狗子竟然敢咬小姐姐,我必须得冲上去挡在前面狠狠地咬这条沙雕狗子。于是我「嗷」地一声冲着狗子猛扑过去,把狗子吓得猛一哆嗦,夹起来的尾巴这一明显的特征已然透露出这狗子已经被我征服了,就在这关键时刻,这姐们儿突然抱起那狗子慌里慌张地跑了,中间还撇了我几眼,从她的眼神中我可以读取到信息:这tm哪儿来的神经病...
实际上人家问了我很多问题,但我今天就想说其中一个:
Redis中Key的过期删除原理大概是怎样的?
题外话:之所以能记起这个问题,完全是因为有个泥腿子说MongoDB支持定时删除数据。
当时这问题可给我整懵了,我那会儿可没怎么深入研究过Redis,所以当时我就开启了胡诌模式,而且一定程度上说我应该是胡诌对了一部分:先胡诌Redis对于过期Key处理的三种策略,再胡诌定时器,但是胡诌定时器的时候实际上心里已经很虚了,和三种策略连贯性上串联地并不好。
现在想想,这个问题真挺简单的,当时只要我再狠狠地吃口屎冷静一下,没准能全部胡诌对了。回到正题上来,Redis到底是如何结合Key的过期策略实现对过期Key删除的?
首先说下业界通用的关于过期Key处置的三种策略:
以上内容,我默认大家都应该是100%知道的。而且请务必注意:以上三种策略是通用策略,而不是Redis的三种策略。Redis里,实现了惰性删除和定期删除两种策略。
这就完了?并没有,我们需要聊下定时器!定时器一般说来是分两种的:
在Redis里应该大多数定时器都是周期性的(我没怎么太详细读过Redis源码,按场景说的话,Redis里应该大量都是周期性定时器,至于定时发生的定时器我不确定是否一定有),那么问题来了:
定时器是怎么实现的呢?
好了上面就是就是一些常见的定时器实现的方法,讲这个就是就是为了铺垫Redis里过期key处理的实现,还是回到面试题本身来。不要妖魔化定时器,实际上TA就是个串串儿。
在Redis里,有一个叫做struct叫做redisDb,这个struct里有一个指针叫做expires的指针,这个指针指向了一个dict(你就粗暴理解为hashtable),然后这个dict(就是个hashtable)里存了所有的被设置了过期时间的key,TA大概是这样样子的:
// 仅仅为演示,具体redis里具体数据结构请参考redis源码
struct redisDb {
dict * expires; // 指向一个dict(就是hashtable,就k=>v)
}
// 就key=>value,key就是键值,value就是过期时间
dict hashtable {
key1 => 1234567 // 过期时间的unix时间戳
key2 => 1234567 // 过期时间的unix时间戳
key3 => 1234567 // 过期时间的unix时间戳
.
..
...
}
所以对于惰性删除这种策略,本质上就是每当你从Redis里获取一个key的时候,系统都会对比当前时间戳与key的过期时间戳,对比一下,这个逻辑我们在常规CURD业务里都经常用,Redis竟然与我们雷同,一定是Redis抄我们的。具体在Redis里,这个业务逻辑流程的函数叫做expireIfNeeded(),有兴趣同学可以仔细关注下。
所以对于定期删除这种策略,就是Redis本身的定期启动扫描,但是每次都是扫描一定数量的key,等扫描一段时间后系统记录下当前扫描位置然后强制结束,等下次再次扫描时候接着从上次终止的地方继续扫描。具体体现在Redis里就是activeExpireCycle()函数,这个函数会定期启动,每次挑选出一定数量的库,然后从每个库中挑选出一定数量的key,进行主动的删除处理,而且每次都是在有限时间内,避免Redis响应主业务出现问题。
那么我们接着唠下「定时删除」这种策略,敞开了唠,放飞自我!如果在Redis里要实现「定时删除」这种策略,方案上应该是要为「每个设置了过期时间的key同时创建一个定时器」,这种定时器会在到达的时候定时器启动主动删除掉key。而目前的Redis里,定时器事件大概是这样的(请注意定时器与定时器事件的区别):
// 仅仅为演示的伪结构
time_event {
int id // 定时器的id,反正是一个数字
long expire_time // 何时执行...
callback process // 到期执行时候要执行哪个函数,就是回调
}
然后所有的定时器事件以「链表」这种数据结构形式串在一起成了一个串串儿,新的定时器事件一定会被添加到「链表」的最前边,成为最强插队者。这个串串儿是无序的,这个无序是说expire_time是无序的,并不是说id无序。
假如说我们为每个设置了过期时间的key都要添加一个定时器事件,那么这个「无序的链表」将会承载很多东西了。因为串串儿是「expire_time」无序的,所以一个key的过期时间到了后定时器会执行后需要遍历整个串串儿找出满足「expire_time」条件的时间事件,这个时间复杂度是O(N)。Redis是一个主流程业务为单进程单线程的服务器,这种定时器以及定时器事件毫无疑问将会要了TA的小命。
所以我们继续放飞自我,还记得前面那张图里一个泥腿子说「Mongodb可以设置数据过期」这个事儿么?如果说下次你面试有面试官问你Mongodb这个大概是如何实现的,虽然你没有接触过Mongodb,但是你总是能根据目前已有的底层原理去大胆猜测一下吧?连推论都不敢推,你还敢对HR说你热爱这家公司?其实这里是个面试的小技巧,虽然面试官问的东西你没有接触过,但是你一定要尝试根据已有的基础知识去推测一下,面试是双向的,面试官对你循循善诱,你也要引导对面试官。
面试官:你能说下Mongodb删除过期数据的怎么实现的吗?
老李想象中的泥:虽然我没接触过,但是我想推测一下,你看我设计的合不合理。
真实的泥:...呃,那个,没怎么用过Mongodb...
然后我们接着放飞自我,你们一定遇到过这种业务场景:当Redis某个key过期后,能不能通知一下我?...宝贝儿们,深入思考下,如果要实现一个稳定靠谱的通知功能,你觉得以目前Redis的现状,能实现么?友情提示:Redis有个关键字叫做keyspace,了解一下?
最后说明一下什么叫「Redis是一个主流程业务为单进程单线程的服务器」,大家虽然都说Redis单进程单线程,这个意思是说Redis的主要逻辑是单进程单线程的,而不是所有流程。众所周知(我假装你知道)Redis在开启了rdb备份后,每次从内存向硬盘dump rdb文件的时候,都是fork出一个子进程执行的dump任务。如果不fork子进程执行这个dump任务,那Redis在dump期间一定是停止响应客户端请求的。所以说,你能说TA单进程单线程吗?所以一般说某某软件为单进程单线程,都是指其主业务流程为单进程单线程。
最后说明一下什么叫「Redis是一个主流程业务为单进程单线程的服务器」,大家虽然都说Redis单进程单线程,这个意思是说Redis的主要逻辑是单进程单线程的,而不是所有流程。众所周知(我假装你知道)Redis在开启了rdb备份后,每次从内存向硬盘dump rdb文件的时候,都是fork出一个子进程执行的dump任务。如果不fork子进程执行这个dump任务,那Redis在dump期间一定是停止响应客户端请求的。所以说,你能说TA单进程单线程吗?所以一般说某某软件为单进程单线程,都是指其主业务流程为单进程单线程。