redis过期策略

REDIS过期策略

简介

在使用redis作为缓存的场景下,内存淘汰策略决定的redis的内存使用效率。在大部分场景下,我们会采用LRU(Least Recently Used)来作为redis的淘汰策略;

LRU:

LRU算法作为内存管理的一种有效算法,其含义是在内存有限的情况下,当内存容量不足时,为了保证程序的运行,这时就不得不淘汰内存中的一些对象,释放这些对象占用的空间;
每次淘汰最近最少使用的元素,一般采用对存储在内存的元素采用 ‘age bits’ 来标记该元素从上次访问到现在为止的时长,从而在每次用LRU淘汰时,淘汰这些最长时间未被访问的元素;
在操作系统中LRU算法淘汰的不是内存中的对象,而是页,当内存中数据不足时,通过LRU算法,选择一页(一般是4KB)将其交换到虚拟内存区(Swap区);
LRU算法最为经典的实现,就是HashMap+Double LinkedList,时间复杂度为O(1);
常见的过期策略

定时删除

含义:
    在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
优点:
    保证内存被尽快释放
缺点:
    若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key
    定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重

惰性删除

含义:
    key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
优点:
    删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步
缺点:
    若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)

定期删除

含义:每隔一段时间执行一次删除过期key操作
优点:
    通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点
    定期删除过期key--处理"惰性删除"的缺点
缺点:
    在内存友好方面,不如"定时删除"
    在CPU时间友好方面,不如"惰性删除"
难点:
    合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)
Redis采用的过期策略:惰性删除+定期删除

定期删除流程(简单而言,对指定个数个库的每一个库随机删除小于等于指定个数个过期key)

遍历每个数据库(就是redis.conf中配置的"database"数量,默认为16)
检查当前库中的指定个数个key(默认是每个库检查20个key,注意相当于该循环执行20次)
如果当前库中没有一个key设置了过期时间,直接执行下一个库的遍历
随机获取一个设置了过期时间的key,检查该key是否过期,如果过期,删除key
判断定期删除操作是否已经达到指定时长,若已经达到,直接退出定期删除。

惰性删除流程

在进行get或setnx等操作时,先检查key是否过期,
若过期,删除key,然后执行相应操作;
若没过期,直接执行相应操作

简单来说:

1.主动淘汰
    1.1 通过定时任务serverCron定期的清理过期的key(serverCron每间隔1000/hz ms会调用databasesCron方法来检测并淘汰过期的key)
2.被动淘汰
    2.1 每次写入key时,发现内存不够,调用activeExpireCycle释放一部分内存
    2.2 每次访问相关的key,如果发现key过期,直接释放掉该key相关的内存
REDIS的过期策略选项

回收内存的过程:

1.客户端发起了需要申请更多内存的命令;
2.Redis检查内存使用情况,如果已使用的内存大于maxmemory则开始根据用户配置的不同淘汰策略来淘汰内存(key),从而换取一定的内存;
3.如果上面都没问题,则这个命令执行成功;

注意:

内存耗尽时,如果你打开虚拟内存功能,Redis就会把那些不经常使用的数据存储到磁盘;
如果Redis里的虚拟内存被禁了,他就会用上操作系统的虚拟内存(交换内存),同时性能急剧下降;
你可以配置maxmemory参数,来避免Redis默认再分配更多的内存;
maxmemory为0的时候表示我们对Redis的内存使用没有限制;

maxmemory-policy:过期策略选项

Redis无论有没有设置expire,他都会遵循redis的配置好的删除机制,其中默认的策略为noeviction策略;
内存淘汰策略主要采用了6种方式进行内存对象的释放操作:

    volatile-lru
        从设置了过期时间的数据集中,选择最近最久未使用的数据释放
    allkeys-lru
        从数据集中(包括设置过期时间以及未设置过期时间的数据集中),选择最近最久未使用的数据释放
    volatile-random
        从设置了过期时间的数据集中,随机选择一个数据进行释放
    allkeys-random
        从数据集中(包括了设置过期时间以及未设置过期时间)随机选择一个数据进行入释放
    volatile-ttl
        从设置了过期时间的数据集中,采取TTL算法(最小存活时间)选择数据进行释放操作
    noeviction
        不删除任意数据(但redis还会根据引用计数器进行释放),这时如果内存不够时,会直接返回错误

默认的内存策略是noeviction,在Redis中LRU算法是一个近似算法,默认情况下,Redis随机挑选5个键,并且从中选取一个最近最久未使用的key进行淘汰;
在配置文件中可以通过maxmemory-samples的值来设置redis需要检查key的个数,但是栓查的越多,耗费的时间也就越久,但是结构越精确;

REDIS的LRU执行机制

在redis内部,是通过全局结构体struct redisServer 保存redis启动之后相关的信息,比如:

    struct redisServer {
       pid_t pid; /* Main process pid. */
       char *configfile; /* Absolute config file path, or NULL */
       …..
       unsigned lruclock:LRU_BITS; /* Clock for LRU eviction */
       ...
    };

redisServer 中包含了redis服务器启动之后的基本信息(PID,配置文件路径,serverCron运行频率hz等),外部可调用模块信息,网络信息,RDB/AOF信息,日志信息,复制信息等等。

上述结构体中lruclock:LRU_BITS,其中存储了服务器自启动之后的lru时钟,该时钟是全局的lru时钟;
该时钟100ms(可以通过hz来调整,默认情况hz=10,因此每1000ms/10=100ms执行一次定时任务)更新一次;

server.lruclock在redis.c中运行的定时器中进行更新操作,代码如下(redis.c中的定时器被配置中100ms执行一次):

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    .....
    run_with_period(100) trackOperationsPerSecond();

    /* We have just REDIS_LRU_BITS bits per object for LRU information.
     * So we use an (eventually wrapping) LRU clock.
     *
     * Note that even if the counter wraps it's not a big problem,
     * everything will still work but some object will appear younger
     * to Redis. However for this to happen a given object should never be
     * touched for all the time needed to the counter to wrap, which is
     * not likely.
     *
     * Note that you can change the resolution altering the
     * REDIS_LRU_CLOCK_RESOLUTION define. */
    server.lruclock = getLRUClock();
    ....
    return 1000/server.hz;
}

在redis.h中声明的redisObj定义了redis key对应的value的存放对象:

#define REDIS_LRU_BITS 24
#define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1) /* Max value of obj->lru */
#define REDIS_LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
typedef struct redisObject {<br>  //存放的对象类型
    unsigned type:4;
    //内容编码
    unsigned encoding:4;
    //与server.lruclock的时间差值
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */\
    //引用计数算法使用的引用计数器
    int refcount;
    //数据指针
    void *ptr;
} robj;

该代码中最为关键的一句就是o->lru=LRU_CLOCK(),这是一个定义,看一下这个宏定义的实现,代码如下所示

#define LRU_CLOCK() ((1000/server.hz <= REDIS_LRU_CLOCK_RESOLUTION) ? server.lruclock : getLRUClock())

其中REDIS_LRU_CLOCK_RESOLUTION为1000,可以自已在配置文件中进行配置,表示的是LRU算法的精度,在这里我们就可以看到server.lruclock的用处了,如果定时器执行的频率高于LRU算法的精度时,可以直接将server.lruclock直接在对象创建时赋值过去,避免了函数调用的内存开销以及时间开销

有了上述的基础,下面就是最为关键的部份了,REDIS中LRU算法,这里以volatile-lru为例(选择有过期时间的数据集进行淘汰),在Redis中命令的处理时,会调用processCommand函数,在ProcessCommand函数中,当在配置文件中配置了maxmemory时,会调用freeMemoryIfNeeded函数,释放不用的内存空间

总结

1.redis做为缓存,经常采用LRU的策略来淘汰数据,所以如果同时过期的数据太多,就会导致redis发起主动检测时耗费的时间过长(最大为250ms),从而导致最大应用超时 >= 250ms;
2.内存使用率过高,则会导致内存不够,从而发起被动淘汰策略,从而使应用访问超时;
3.合理的调整hz参数,从而控制每次主动淘汰的频率,从而有效的缓解过期的key数量太多带来的上述超时问题;

参考:
https://www.cnblogs.com/WJ5888/p/4371647.html
https://blog.csdn.net/caishenfans/article/details/44902651
http://blog.jobbole.com/105335/
http://blog.jobbole.com/107084/

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值