过期Key处理:
1)Redis之所以性能强大,最主要的原因就是基于内存来存储,然而单节点的Redis内存不宜设置的过大,否则会影响持久化或者是主从复制的性能,可以通过修改配置文件来设置redis的最大内存,通过maxmemory 1gb,但是当内存达到上限的时候,就无法存储更多数据了
2)在之前学习Redis缓存的时候,可以通过expire命令来给Redis的key设置TTL过期时间
3)可以发现,当key的ttl过期之后,再一次访问name的时候返回的是nul,说明这个key已经不存在了,对应的内存也就释放了,从而起到了内存回收的目的
4)Redis本身是一个典型的key-value键值对的内存存储的数据库,因此所有的key和value都是保存在之前学习过的Dict结构中,但是在database结构体中有两个Dict,一个是用来记录key-value,一个用来记录key-ttl;
在设置键的过期时间的同时,设置一个定时器,当键过期了,定时器马上把该键删除
定时删除对内存来说是友好的,因为它可以及时清理过期键,但对CPU是不友好的,如果过期键太多,删除操作会消耗过多的资源
在过期 key 比较多的情况下,删除过期 key 这一行为可能会占用相当一部分 CPU 时间,在内存不紧张但是 CPU 时间非常紧张的情况下,将 CPU 时间用在删除和当前任务无关的过期 key 上,无疑会对服务器的响应时间和吞吐量造成影响
Redis采用的是 惰性删除 + 定期删除 的策略
一)惰性删除:顾名思义就是说并不是在TTL到期之后就立即删除,而是在访问一个Key的时候(增删改查),检查该Key的存活时间,如果过期了才执行删除;
当查询数据的时候,首先根据key查询对应的expires,检查是否过期,如果过期就返回null,并删除对应的数据和expires,如果不过期,就返回数据
假设给一个key设置了过期时间,但是永远不会访问这个key,那么这个key就永远也不会被删除,这样就会导致内存占用过多的状况
这种方式看似很完美,在访问的时候检查key的过期时间,不会占用太多的额外CPU资源但是如果一个key已经过期了,如果长时间没有被访问,那么这个key就会一直存留在内存之中,严重消耗了内存资源,并且删除的目标仅限于当前处理的 key不会在删除其他无关的过期 key 上花费任何 CPU 时间
二)周期删除:顾名思义就是设置一个定时任务,周期性的抽样部分过期的key,然后执行删除,执行周期有两种:
定期删除是定时删除和惰性删除的一个折中方案,每隔一段时间来删除过期键,可以根据实际场景自定义这个间隔时间,在CPU资源和内存资源上作出权衡
2.1)Redis初始化会设置一个定时任务serverCron(),按照server.hz(1s中最多执行1次)的频率来执行过期的key的清理,模式为slow,过期的key,早晚会被抽到,执行时间长,执行的频率比较低;
2.2)Redis初始化后的1ms去执行
2.3)serverCron的返回值是完成serverCron再隔多少毫秒之后执行此定时任务(100ms)
2.4)getLRUClock时钟是更新lruclock到当前时间,为后期的LRU和LFU做准备,并且接下来会调用atomicSet方法设置到server.lruclock里面,lruclock是redis内部维护的一个时钟,是以微秒为单位的,每隔一个周期会进行记录一次,每隔100ms会更新一次,每调用一次serverCron方法lruclock是不断的会发生变化的
2.5)开始执行数据库的数据清理,例如说过期的key的清理;
2.2)Redis的每一个事件循环前会调用beforeSleep()函数,执行过期的key清理,模式为FAST,执行频率比较高,执行的时间非常短,不超过1ms
1)redis服务一进行启动,先调用initserver()函数,完成各种各样的注册,创建ServerSocket以及eventpoll,将ServerSocket对应的fd挂到红黑树上面;
2)接下来会执行aemain来进行事件循环,开启一个事件循环,会不断的调用beforesleep,再来调用aeapipoll,相当于是epoll_wait,等待事件就绪,如果事件就绪了,就进行对应的Socket的读写,处理IO事件;
3)最后会调用serverCron()进行定时任务的清理,但是除了redis在初始化之后1ms调用serverCron之外,以后的serverCron执行完成之后都会返回一个1000/server.sz,返回的就是100ms,默认就是下一次aeApiPoll执行的时间,所以当while循环执行的时候程序会进行检查serverCron()执行的时间到了没有,如果时间到了才执行,确保每隔100ms执行一次,时间不到就不执行serverCron函数了,因为while循环速度非常快,如果在每一次循环中都去调用serverCron()函数,那么这个函数执行的频率就会非常高了,但是再beforeSleep模式中,Fast模式清理是随着循环而进行的,每一次执行while循环,都会执行fast回收;
4)因为fast执行的模式是非常快的,清理速度也是非常快的,一次执行的速度会控制在1ms以内,而slow模式可能执行一次可能是消耗几十毫秒,如果serverCron()每循环一次都要执行,每一次主线程执行while循环都需要卡顿几十毫秒,那么主线程执行性能将会非常低,所以slow模式并不是在每一次循环都执行,而是每隔100ms之后执行,避免主线程阻塞;
slow模式属于是低频,长时间的清理,清理效果会更好一点,可以清理更多的Key
fast模式属于高频,少量清理,耗时时间非常的短,最长不超过1ms
但是他们都是为了在不阻塞主线程的情况下,尽可能过多的清理过期的key
slow模式规则:低频高时长的执行
1)执行频率受server.hz影响,默认是10,即每一秒执行10次,每一个执行周期是100ms
2)执行清理key的耗时不超过一次执行周期的25%;
3)逐个遍历db,逐个遍历db中的bucket,抽取20个key查看他们是否过期,直到把所有的buket都遍历到,如果发现过期的key就直接干掉;
4)如果没有达到时间上线25ms况且过期key的比例超过10%,那么再次进行一次抽样,否则结束;
fast模式规则:过期key的比例小于10%不执行,高频低时长
1)执行频率受beforesleep()函数调用的影响,但是两次fast模式执行的间隔不应该低于2ms,fast模式每一次执行前都会判断上一次fast到现在的时间,如果不足2ms就会跳过;
2)执行清理的耗时不超过1ms
3)遍历整个db,逐个遍历db中的bucket,抽取20个key判断是否过期
4)如果没有达到时间上限1ms,况且过期key的比例超过10%,那么在进行一次抽样否则结束
Redis 中,flushall 和 flushdb 都是清空当前数据库的操作,但是两者有很大的区别:
1)flushall 清空数据库并执行持久化操作,也就是 rdb 文件会发生改变,变成 76 个字节大小(初始状态下为 76 字节),所以执行 flushall 之后数据库真正意义上清空了
2)flushdb 清空数据库,但是不执行持久化操作,也就是说 rdb 文件不发生改变。而 Redis 的数据是从 rdb 快照文件中读取加载到内存的,所以在 flushdb 之后,如果想恢复数据库,则可以直接 kill 掉 redis-server 进程,然后重新启动服务,如此 Redis 重新读取 rdb 文件,数据恢复到 flushdb 操作之前的状态;
3)注意:要直接 kill 掉 redis-server 服务,因为 shutdown 操作会触发持久化,lsof -i:6379命令查看 redis-server 的进程号,然后 kill 即可
redis的内存淘汰策略:
1)redis的过期策略可以将redis中一些过期的key直接删除,但是在一些庞大的项目中,因为数据量非常的多,请求不断地向redis中进行存储数据,很有可能仅仅淘汰过期的key也很难满足内存的使用,此时内存也有可能达到上限;
2)内存淘汰:就是当Redis的内存使用达到设定的阈值的时候,Redis主动挑选部分key删除以释放更多的内存的流程
redis在每一次执行命令的时候,都会进行检查,看看服务器内存是否够用
实际上,Redis支持8种不同的策略来选择要删除的key:
1)noeviction:不淘汰任何key,但是内存满的时候不允许写入任何新数据,默认就是这种策略
2)volatile-ttl:对设置了TTL的key,比较key的剩余ttl的值,ttl越小越先进行淘汰
3)allkeys-random:对于全体key,随机来进行淘汰,也就是从db->dict中进行挑选
4)volatile-random:对设置了ttl中的key随机进行淘汰,也就是从db->expires中进行挑选
LRU:Least Recently Used,最少最近使用,用当前时间减去最后一次访问时间(就是你多久没有被访问了),这个值越大淘汰的优先级就会越高,代表很长时间没有被访问了
LFU:Least Frequently Used,最少频率使用,会统计每一个key的使用频率,值越小淘汰率优先级越高
那么Redis是如何统计最近访问的时间和最近访问的频率呢?
Redis中所有存储的键值对最后都被封装成了RedisObject
1)当配置文件中指定的内存淘汰策略不同,实际上lru中记录的值也是不同的
Lru的访问次数之所以叫逻辑访问次数是因为并不是每一次key访问都会被计数而是用过运算:
2)但是第一次访问的时候P计数器一定是+1的,后续的访问计数器的次数不一定会累加,完全取决于概率,对这个key访问的次数越多,分母的值就越大,P的值就越小,R<P的概率也越小,计数器累加的概率也是越小的;
3)访问次数衰减,以当前时间为单位,减去上一次访问的时间,上一次访问的时间再LFU的高16位中存着呢;
1)首先会进行判断内存是否充足,就是已经使用的内存是否已经超过内存使用的最大值,如果内存充足就直接返回了;
2)如果内存不充足,redis会进行判断内存淘汰策略是否是noeviction,客户端就会报错;
3)接下来Redis会进行判断内存淘汰策略是根据所有key来进行淘汰还是根据过期的key进行淘汰,判断是否是AllKeys;
4)接下来redis会进行判断内存淘汰策略,如果是Random策略,就根据第三步选择的是在所有key中进行淘汰还是在过期key中进行淘汰,遍历DB随机选取一个key进行淘汰;
5)每一次删除完成之后都会去进行判断已经释放的内存是否满足于内存的需要,如果不满足就直接返回第四步,进行判断内存淘汰策略,尝试进行删除key;
6)如果在第三步中,内存淘汰策略不是随机的,那么有可能是ttl越小越先进行淘汰,LRU和LFU,都要判断TTL的值,进行比较访问次数/TTL值/上一次访问的时间,但是不能一个一个的key进行比较,如果通过遍历的方式进行比较,那么会导致此次内存淘汰的性能将会变得非常低,为了解决这个问题,Redis内部内置了一个eviction_pool,这个池子将来会对存放到里面的key中的某一个值做升序排列,将来值越大的越先淘汰,这个值是idleTime
7)Redis会先获取到Redis中随机的一个数据库,拿到这个数据库以后,然后随机进行挑选5个key,但是这5个key不一定都会被扔到eviction_pool池子里面,还要从这5个key中做筛选,接下来就通过不同的策略来执行筛选的标准;
7.1)对于volatile-ttl来说,使用maxTTL-TTL作为idleTime;
7.2)对于LRU来说,使用now-LRU值作为idleTime
7.3)对于LFU来说,使用255-LRU值作为idleTime
8)然后判断这个key是否应该存放到eviction_pool里面,如果池子已经满了,那么Redis会进行判断当前key的idleTime和池子中的一个key的idle值要大,如果要存放的key的idle值要比池子中的idle值大,那么就将这个key存放进去,并删除idle最小的key
9)接下来再来循环处理下一个数据库,重复第七步
10)接下来倒序从池子里面获取一个key进行删除,判断删除完成之后内存是否充足,如果不充足,返回第三步;