在开发场景中,我们会有一场景使用到过期策略,设置缓存的过期时间,来看看底层怎么实现的吧。
当设置一个key过期的时候,redis会将这个key以及过期时间存储到过期字典中,其实就是将key和过期时间一起保存到一个字典中,方便管理。
typedef struct redisDb {
dict *dict; // 主字典:存储 key -> value
dict *expires; // 过期字典:存储 key -> 过期时间(毫秒时间戳)
...
} redisDb;
dict *dictredis的主数据字典,存储实际的key-valuedict *expiresredis的过期字典,存储形式为:key --> expires(过期时间)单位毫秒
这两个字典的key是共享的,如果一个key没有设置过期时间,它只存在dict *dict字典中;如果设置了过期时间,就同时存在dict *expires和dict *dict这两个字典中。
redis 删除策略
- 定时删除
- 在设置一个key的过期时间时,同时创建一个定时事件,定时时间到由事件处理器自动执行删除操作
- 这样可以准时高效将key删除掉,释放空间。
- 但是如果很多key都设置了过期时间,也就是要创建多个事件去删除,对CPU不友好
- 惰性删除
- 不主动删除过期的key,直到访问到key然后检查过期时间是否需要删除
- 访问key的时候再去检查过期时间的操作,对CPU友好一些,就不用创建定时事件区管理
- 但是如果key已经过期,这个key没有被访问到,就会一直占用内存空间,对内存不友好。
- 不主动删除过期的key,直到访问到key然后检查过期时间是否需要删除
- 定期删除
- 每隔一段时间,随机从数据库中取一定数量的key进行检查,并删除其中过期的key
- 通过限制删除操作执行的时长和频率,减少对cpu的影响,同时也可以删除一些过期的数据
- 内存清理方面没有定时删除的效果好,同时没有惰性删除使用的系统资源少
- 每隔一段时间,随机从数据库中取一定数量的key进行检查,并删除其中过期的key
惰性删除+定期删除(redis采用)
-
redis在访问或修改key的时候,都会调用函数expireIfNeeded进行检测,检查key是否过期
- 如果过期,直接删除key。可设置
lazyfree_lazy_expire参数进行配置同步或异步 - 如果未过期,不进行处理,直接返回数据给客户端
- 如果过期,直接删除key。可设置
-
定期删除:每隔一段时间从数据中取一定量的key,然后检测是否过期,过期并删除
- 默认每10秒从数据库中取,
hz配置默认为10s,可自行配置。每次都是随机的取,并不是遍历过期字典中的所有key。 - 从过期字典中随机抽取20个key;检查这20个key是否过期并删除过期的key;、
- 如果这20个key中有超过5个已经过期的key(也就是25%),那么就继续随机抽取20个,直到过期key小于25%位为止
- 为防止出现循环过度,导致线程卡死的现象出现,设置定期删除的循环流程的时间限制不超过25ms
redis 内存淘汰策略
-
不进行数据淘汰策略
noeviction:当前运行内存超过最大内存设置时,不淘汰任何数据。如果这个时候有新的数据写入,会报错通知禁止写入,不淘汰任何数据。如果没有数据写入的情况下,可以正常工作。 -
进行数据淘汰策略
设置了过期时间的数据范围中淘汰
volatile-random:随机淘汰设置了过期时间的任意键值对。
volatile-ttl:优先淘汰更早过期的键值对。
volatile-lru:淘汰所有设置过期时间的键中,最久未使用的键值。
volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值。
所有数据范围
allkeys-random:随机淘汰任意键值。
allkeys-lru:淘汰整个键值中最久未使用的键值。
allkeys-lfu:淘汰整个键值中最少使用的兼职。
Redis的LRU算法
Redis的实现方式为对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。当redis进行内存淘汰的时,会使用随机采样的方式来淘汰数据,随机取5个值(可配置),然后淘汰最久没有使用的。
算法优点:不需要为所有的数据维护一个大链表,节省占用空间;不在每次访问数据的时候移动链表。传统的LUR算法使用的是链表结构,然后将常用的数据往前移动(这里的移动有开销),如果频繁的移动数据是对性能有影响的。
算法缺点:无法解决缓存污染的问题,如一次读取一定量的数据,而这些数据只会被读取一次,那么这些数据会停留在redis中很久,造成污染。
Redis的LFU算法
传统的LFU算法是根据访问次数来进行淘汰数据的,核心为数据访问多次,那么将来被访问的频率页更高。所以每次访问的数据的时候,就会增加数据访问次数。这样可以解决偶尔一次访问之后,数据留存在缓存中很长一段时间的问题。
redis实现的LFU算法:
typedef struct redisObject {
unsigned type:4; // 数据类型(string/list/set/zset/hash)
unsigned encoding:4; // 编码方式(raw/int/embstr/hashtable/...)
unsigned lru:24; // LRU 时间戳或 LFU 信息
int refcount; // 引用计数
void *ptr; // 指向实际存储的数据
} robj;
在redisObject中有一个字段lru字段占用24bit位,在LRU算法中记录的是访问时间戳。LFU中该字段的高16bit(ldt)位记录的访问的时间戳,低8bit位(logc)记录的是访问的频率(值越小频率越低,越容易被淘汰)。
logc记录的是访问频率,会随时间推移而逐渐衰减。
每次访问key时,对logc做一个衰减的操作,衰减的值跟前后访问文件的差距有关系,如果上一次访问的时间与这一次访问的时间差距很大,那么衰减值就会越大,这样实现的LFU算法根据访问频次来淘汰数据,而不是访问次数。
访问频率要考虑key的访问是多长时间段内发生的。key的先前访问时间距离当前时间越长,那么这个key的访问频率相对应的会降低,被淘汰的概率就更大。
对logc做完衰减操作后,就开始对logc进行增加操作,增加操作并不是单纯的+1操作,而是根据概率增加,如果logc越大的key那么logc就越难增加。
变化情况:
- 先按照上次的访问时间和当前的访问时间算出时长,然后对logc进行衰减
- 之后按照一定的概率增加logc的值
logc衰减配置:
lfu-decay-time:用于调整logc的衰减速度,它是一个以分钟为单位的值,默认1分钟。值越大,衰减越慢lfu-log-factor:用于调整logc的增长速度,当前值越大,logc增长越慢。