Redis 过期Expires

Redis对存储值得过期处理实际上是针对该值得键(key)处理的,即时间的设置也是设置key的有效时间
设置key的过期时间,超过时间后,将会自动删除key;

一、设置过期时间

  • expire key time(以秒为单位):最常见的方式
  • pexpire key time(以毫秒为单位)
  • setex key seconds value :字符串独有的方式;设置值并且给过期时间,如果key已经存在,setex命令将会替换旧值;
  • expireat key timestamp 将key的过期时间设置为timestamp所代表的秒数的时间戳
  • pexireat key timestamp 将key的过期时间设置为timestamp所代表的毫秒数的时间戳

注:expire和pexpire 是设置一个过期的时间段,比如设置几分钟后过期或者几秒钟后过期;而expireat和pexpireat是指定一个过期时间,比如优惠券在某年某月某日过期

// expire用法
127.0.0.1:6379> set age 12
OK
127.0.0.1:6379> get age
"12"
127.0.0.1:6379> expire age 10
(integer) 1
127.0.0.1:6379> ttl age
(integer) 8
127.0.0.1:6379> ttl age
(integer) 6
127.0.0.1:6379> ttl age
(integer) 1
127.0.0.1:6379> get age
(nil)


//expireat 
127.0.0.1:6379> get age
"18"
127.0.0.1:6379> time
1) "1637996753"//2021-11-27 15:05:53
2) "982529"
127.0.0.1:6379> expireat age 1637996825 //吧时间戳设置为2021-11-27 15:07:05为过期时间
(integer) 1
127.0.0.1:6379> ttl age
(integer) 41
127.0.0.1:6379> ttl age
(integer) 39
127.0.0.1:6379> ttl age
(integer) 36
127.0.0.1:6379> ttl age
(integer) 27
127.0.0.1:6379> ttl age
(integer) 9
127.0.0.1:6379> get age
"18"
127.0.0.1:6379> get age
(nil)


//setex

127.0.0.1:6379> get age
(nil)
127.0.0.1:6379> set age 28
OK
127.0.0.1:6379> get age
"28"
127.0.0.1:6379> setex age 30 38
OK
127.0.0.1:6379> get age
"38"
127.0.0.1:6379> ttl age
(integer) 22
127.0.0.1:6379> ttl age
(integer) 19
127.0.0.1:6379> ttl age
(integer) 4
127.0.0.1:6379> ttl age
(integer) -2
127.0.0.1:6379> get age
(nil)

特殊情况
1、persist 可以清除超时,使其变成一个永久的key;expire的反命令;
2、如果key被rename命令修改,相关的超时时间就会移到新key上面;


127.0.0.1:6379> setex age 20 18
OK
127.0.0.1:6379> get age
"18"
127.0.0.1:6379> rename age new_age
OK
127.0.0.1:6379> ttl age
(integer) -2
127.0.0.1:6379> ttl new_age  //age的有效期移到了new_age上面
(integer) 5
127.0.0.1:6379> ttl new_age
(integer) -2

过期精度

在Redis2.4以前版本,过期时间可能不是十分准确,有0-1秒的误差;
在Redis2.6之后,过期时间误差缩小到0-1毫秒;

二、Redis如何淘汰过期的keys

Redis keys过期的方式有两种:
1、被动(惰性删除):当一些数据过期后,并不会马上删除,而是有客户端尝试访问它时,对数据进行检查,如果数据过期,则删除数据;

优点:不需要单独启动额外的扫描线程,减少了CPU资源的消耗;
缺点:大量的过期数据滞留在内存中,需要主动触发、检查、删除、否则这些过期的key会一直占用内存资源;

2、定期删除:每隔一段时间,默认100ms,Redis会随机挑选一定数量的key,检查是否过期,并将过期的数据删除;

被动删除的步骤:
1、测试随机的设置的个keys进行相关过期检测;
2、删除所有已经过期的keys;
3、如果有多于25%的keys过期,重复步骤1;

3、数据淘汰策略【具体内容会在内存优化方面详细说】

当内存超过maxmemory限定时,也会触发主动清理策略

虽然我们设置了Redis key的过期时间,但如果业务应用写入量很大,并且过期时间设置的比较久,那么短时间内Redis的内存还是会快速增长
对于这种场景,需要提前预估业务数据量,然后给这个实例设置maxmemory控制实例的内存上限,这样就可以避免Redis的内存持续膨胀;

设置数据淘汰策略,则具体业务场景具体分析了;

volatile-lru / allkeys-lru:优先保留最近访问过的数据
volatile-lfu / allkeys-lfu:优先保留访问次数最频繁的数据(4.0+版本支持)
volatile-ttl :优先淘汰即将过期的数据
volatile-random / allkeys-random:随机淘汰数据

Redis 中过期键的删除策略正是惰性删除与定期删除的结合。

三、过期键与持久化(看完持久化可能会修改下面内容)

持久化的两种方式:RDB和AOF;

对于过期也分开讨论:

在生成RDB的文件的过程中,如果一个键已经过期了,那么其不会被保存到RDB文件中。在载入RDB的时候,分两种情况:
1、如果Redis以主服务器的模式运行,那么会对RDB中的键进行时间检查,过期的键不会被恢复到Redis中。
2、如果Redis以从服务器的模式运行,那么RDB中所有的键都会被载入,忽略时间的检查。在从服务器与主服务器进行数据同步的时候,从服务器的数据会先被清空,所以载入的过期键不会有问题。

对于AOF来说,如果一个键过期了,那么不会立刻对AOF文件造成影响。因为Redis使用的是惰性删除+定期删除,只有这个键被删除了,才会往AOF文件里追加一条del删除命令。在重写AOF的过程中,会检查数据库中的键,已经过期的键不会被保存到AOF文件中。

在运行的过程中,对于主从复制的Redis,主服务器和从服务器对于过期键的处理也不相同:
1、对于主服务器,一个过期的键被删除了后,会想从服务器发送del命令,通知从服务器删除对应的键;
2、从服务器接受到读取一个键的命令时,即使这个键已经过期,也不会删除,而是照常处理这个命令。
3、从服务器接收到主服务器的del命令,才会删除对应的过期键。Redis 2.6之后,从服务器只允许执行读命令,为了确保与主服务器进行数据同步得到更新,保证主从服务器之间数据一致性;

四、源码分析


void expireGenericCommand(client *c, long long basetime, int unit) {
    robj *key = c->argv[1], *param = c->argv[2];
    long long when; /* unix time in milliseconds when the key will expire. */

    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != C_OK)
        return;

    if (unit == UNIT_SECONDS) when *= 1000;
    when += basetime;

    /* No key, return zero. */
    if (lookupKeyWrite(c->db,key) == NULL) {
        addReply(c,shared.czero);
        return;
    }

    /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past
     * should never be executed as a DEL when load the AOF or in the context
     * of a slave instance.
     *
     * Instead we take the other branch of the IF statement setting an expire
     * (possibly in the past) and wait for an explicit DEL from the master. */
    if (when <= mstime() && !server.loading && !server.masterhost) {
        robj *aux;

        int deleted = server.lazyfree_lazy_expire ? dbAsyncDelete(c->db,key) :
                                                    dbSyncDelete(c->db,key);
        serverAssertWithInfo(c,key,deleted);
        server.dirty++;

        /* Replicate/AOF this as an explicit DEL or UNLINK. */
        aux = server.lazyfree_lazy_expire ? shared.unlink : shared.del;
        rewriteClientCommandVector(c,2,aux,key);
        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);
        addReply(c, shared.cone);
        return;
    } else {
        setExpire(c,c->db,key,when);
        addReply(c,shared.cone);
        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id);
        server.dirty++;
        return;
    }
}

/* EXPIRE key seconds */
void expireCommand(client *c) {
    expireGenericCommand(c,mstime(),UNIT_SECONDS);
}

你可能感兴趣的:(性能优化专题,redis)