Redis键的生存时间和过期时间 - Go语言中文社区

Redis键的生存时间和过期时间


一、设置生存时间

        Redis作为内存数据库,和memcached一样提供了设置键的生存时间和过期时间的功能。通过expire命令或者pexpire命令实现秒级或者毫秒级的生存时间的设置功能:

  • EXPIRE <KEY> <TTL> : 将键的生存时间设为 ttl 秒
  • PEXPIRE <KEY> <TTL> :将键的生存时间设为 ttl 毫秒
  • EXPIREAT <KEY> <timestamp> :将键的过期时间设为 timestamp 所指定的秒数时间戳
  • PEXPIREAT <KEY> <timestamp>: 将键的过期时间设为 timestamp 所指定的毫秒数时间戳.

虽然有多种不同单位和不同形式的设置命令,但实际上EXPIRE、PEXPIRE、EXPIREAT、PEXPIREAT四个命令内部都是调用同一个函数实现,在db.c 文件中,四个命令对应的函数如下:

//expire命令
void expireCommand(redisClient *c) {
    expireGenericCommand(c,mstime(),UNIT_SECONDS);
}
//expiread命令
void expireatCommand(redisClient *c) {
    expireGenericCommand(c,0,UNIT_SECONDS);
}
//pexpire命令
void pexpireCommand(redisClient *c) {
    expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
}
//pexpireat命令
void pexpireatCommand(redisClient *c) {
    expireGenericCommand(c,0,UNIT_MILLISECONDS);
}
void expireGenericCommand(redisClient *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. */
    // 取出 when 参数
    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
        return;
    // 如果传入的过期时间是以秒为单位的,那么将它转换为毫秒
    if (unit == UNIT_SECONDS) when *= 1000;
    when += basetime;
    /* No key, return zero. */
    // 取出键
    if (lookupKeyRead(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.
     * 在载入数据时,或者服务器为附属节点时,
     * 即使 EXPIRE 的 TTL 为负数,或者 EXPIREAT 提供的时间戳已经过期,
     * 服务器也不会主动删除这个键,而是等待主节点发来显式的 DEL 命令。
     * 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. 
     * 程序会继续将(一个可能已经过期的 TTL)设置为键的过期时间,
     * 并且等待主节点发来 DEL 命令。
     */
    if (when <= mstime() && !server.loading && !server.masterhost) {
        // when 提供的时间已经过期,服务器为主节点,并且没在载入数据
        robj *aux;
        redisAssertWithInfo(c,key,dbDelete(c->db,key));
        server.dirty++;
        /* Replicate/AOF this as an explicit DEL. */
        // 传播 DEL 命令
        aux = createStringObject("DEL",3);
        rewriteClientCommandVector(c,2,aux,key);
        decrRefCount(aux);
        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);
        addReply(c, shared.cone);
        return;
    } else {
        // 设置键的过期时间
        // 如果服务器为附属节点,或者服务器正在载入,
        // 那么这个 when 有可能已经过期的
        setExpire(c->db,key,when);
        addReply(c,shared.cone);
        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);
        server.dirty++;
        return;
    }
}

        可以看到以上四条命令最终都调用expireGenericCommand函数执行。在redis.h/redisDb的结构的expires字典保存了数据库中所有键的过期时间,这个字典称为过期字典。

  • 过期字典是一个指针,指向键空间的某个键对象
  • 过期字典的值是一个 long long 类型的整数,这个整数保存了键所指向的数据库键的过期时间–一个毫秒级的 UNIX 时间戳
typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires;
    //...
} redisDb;

因此,上一节数据库键空间的基础上加上过期时间:


其键空间变更为以下状态(在实际代码中,键空间的键和过期时间的键都指向同一个键对象):


一旦键过了过期时间,再通过Redis数据库取该键值对就为空:


二、移除过期时间

        persist命令就是pexpireat命令的反操作:persist命令在过期字典中查找给定的键,并解除键值在过期字典中的关联。如果对上述键空间执行:persist message、persist book,其键空间变更为如下状态:


三、计算并返回键的剩余时间

ttl或者pttl命令可以返回秒级或者毫秒级别的键的剩余生存时间。

//ttl命令
void ttlCommand(redisClient *c) {
    ttlGenericCommand(c, 0);
}
//pttl命令
void pttlCommand(redisClient *c) {
    ttlGenericCommand(c, 1);
}
/*
 * 返回键的剩余生存时间。
 * output_ms 指定返回值的格式:
 *  - 为 1 时,返回毫秒
 *  - 为 0 时,返回秒
 */
void ttlGenericCommand(redisClient *c, int output_ms) {
    long long expire, ttl = -1;

    /* If the key does not exist at all, return -2 */
    // 取出键
    if (lookupKeyRead(c->db,c->argv[1]) == NULL) {
        addReplyLongLong(c,-2);
        return;
    }
    /* The key exists. Return -1 if it has no expire, or the actual
     * TTL value otherwise. */
    // 取出过期时间
    expire = getExpire(c->db,c->argv[1]);
    if (expire != -1) {
        // 计算剩余生存时间
        ttl = expire-mstime();
        if (ttl < 0) ttl = 0;
    }
    if (ttl == -1) {
        // 键是持久的
        addReplyLongLong(c,-1);
    } else {
        // 返回 TTL 
        // (ttl+500)/1000 计算的是渐近秒数
        addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000));
    }
}

四、过期键的删除策略

通过过期字典,程序可以通过以下步骤检查一个键是否过期:

(1)检查给定键是否存在于过期字典中,如果存在则取出其过期时间;

(2)检查当前UNIX时间戳是否大于键的过期时间,如果是则键过期执行过期删除;

对于过期键的删除,有三种策略:

(1)定时删除:在设置键的过期时间时,创建一个定时事件,当过期时间到达,由事务处理器自动执行过期键删除的操作。定时删除策略的优点是对内存友好缺点是对CPU不友好,查找一个时间事件的时间复杂度为O(N),所以不能高效处理大量时间事件。

(2)惰性删除:每次从键空间获取键时,检查键是否过期,如果过期则删除,否则返回对应的值。惰性删除策略的优点是对CPU友好缺点是对内存不友好

(3)定期删除:每过一段时间,对数据库检查一次,删除里面的过期键。整合了定时删除和惰性删除的策略进行一定程度的折中。

在db.c/expireIfNeeded函数中可以看到所有命令在读取或写入数据库之前,都会调用expireIfNeeded对键进行检查:

int expireIfNeeded(redisDb *db, robj *key) {
    mstime_t when = getExpire(db,key);
    mstime_t now;
    if (when < 0) return 0; /* 没有设置过期时间 */
    /* 如果服务器正在加载数据,稍后再处理 */
    if (server.loading) return 0;
    ...
    /* 没有过期 */
    if (now <= when) return 0;
    /* 删除键和过期时间 */
    server.stat_expiredkeys++;
    /*将删除命令传播到 AOF 文件和附属节点*/
    propagateExpire(db,key);
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);
    return dbDelete(db,key);
}

参考文献
1、http://www.redis.net.cn/tutorial/3506.html
2、《Redis设计与实现》第二版---黄健宏
3、https://github.com/xingzhexiaozhu/redis-3.0-annotated
4、http://www.yiibai.com/redis/redis_strings.html

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/u012050154/article/details/78653062
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-06-28 02:07:26
  • 阅读 ( 1668 )
  • 分类:Redis

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢