当前位置: 代码网 > it编程>数据库>Redis > Redis过期时间的设计与实现代码

Redis过期时间的设计与实现代码

2024年08月25日 Redis 我要评论
1. 设置过期时间redis 提供了多个命令来设置键的过期时间,如 expire、pexpire、expireat 和 pexpireat。这些命令可以以秒或毫秒为单位设置键的过期时间,也可以设置具体

1. 设置过期时间

redis 提供了多个命令来设置键的过期时间,如 expirepexpireexpireatpexpireat。这些命令可以以秒或毫秒为单位设置键的过期时间,也可以设置具体的过期时间点。

  • expire key seconds
  • pexpire key milliseconds
  • expireat key timestamp
  • pexpireat key milliseconds-timestamp

示例:

void expirecommand(client *c) {
    long long seconds;
    if (getlonglongfromobjectorreply(c, c->argv[2], &seconds, null) != c_ok)
        return;
    setexpire(c, c->db, c->argv[1], mstime() + seconds*1000);
    addreply(c, shared.cone);
}

2. 过期键的存储结构

每个 redis 数据库实例(redisdb)中都有一个名为 expires 的字典,用于存储键的过期时间。这个字典将键指针映射到以毫秒为单位的到期时间点。

typedef struct redisdb {
    dict *dict;                // 主字典,存储所有键值对
    dict *expires;             // 过期字典,存储键的过期时间
    ...
} redisdb;

3. 设置过期时间

通过 setexpire 函数设置键的过期时间。如果键已经存在于 expires 字典中,则更新其过期时间;否则,将其添加到 expires 字典中。

void setexpire(client *c, redisdb *db, robj *key, long long when) {
    dictentry *de = dictfind(db->dict, key->ptr);
    if (de == null) return;

    /* set the new expire time */
    if (dictadd(db->expires, dictgetkey(de), (void*)when) == dict_err) {
        dictreplace(db->expires, dictgetkey(de), (void*)when);
    }
}

4. 删除过期键的策略

redis 采用了以下三种策略来删除过期键:

  • 惰性删除(lazy deletion) :每次访问键时检查其是否过期,如果已过期则删除。这样只在访问键时才进行过期检查,节省了资源。
robj *lookupkeyread(redisdb *db, robj *key) {
    robj *val;
    expireifneeded(db,key);  // 检查并删除过期键
    val = lookupkey(db,key,lookup_none);
    return val ? val : null;
}
  • 定期删除(periodic deletion) :redis 会周期性地随机抽取一定数量的键进行过期检查,并删除其中已过期的键。这一过程由后台任务定期执行,确保尽可能多的过期键被及时删除。
int activeexpirecycle(int type) {
    unsigned int current_db = server.dbnum;
    long long start = ustime();
    long long timelimit = 1000000; // 1秒
    int dbs_per_call = cron_dbs_per_call;

    current_db = server.current_db;
    while(dbs_per_call--) {
        redisdb *db = server.db + (current_db % server.dbnum);
        activeexpirecycletryexpire(db, cycle_tickets);
        current_db++;
    }

    long long elapsed = ustime()-start;
    return elapsed > timelimit;
}
  • 主动删除(active expiration) :在内存使用接近最大限制时,会触发主动删除策略,通过扫描所有库的键删除过期数据,以确保内存使用量保持在设定范围内。
void evictexpiredkeys() {
    for (int j = 0; j < server.dbnum; j++) {
        redisdb *db = server.db+j;
        scandatabaseforexpiredkeys(db);
    }
}
  • redis 默认采用以下两种删除过期键策略:

    惰性删除(lazy deletion) :每次访问某个键时检查其是否过期,如果过期则删除。

    定期删除(periodic deletion) :后台任务定期扫描数据库中的键,随机抽取部分键进行过期检查并删除其中已过期的键。

5. 检查并删除过期键

expireifneeded 函数用于检查某个键是否过期,如果过期则删除该键。

int expireifneeded(redisdb *db, robj *key) {
    mstime_t when = getexpire(db, key);
    if (when < 0) return 0;

    if (mstime() > when) {
        server.stat_expiredkeys++;
        propagateexpire(db,key);
        dbdelete(db,key);
        return 1;
    } else {
        return 0;
    }
}
  • getexpire:从 expires 字典中获取键的过期时间。
  • mstime:返回当前的毫秒时间戳。
  • 如果键已过期,则调用 dbdelete 删除该键,并增加统计计数器 stat_expiredkeys

6. 获取过期时间

getexpire 函数用于获取键的过期时间,如果键没有设置过期时间则返回 -1。

mstime_t getexpire(redisdb *db, robj *key) {
    dictentry *de;
    if (dictsize(db->expires) == 0 ||
        (de = dictfind(db->expires, key->ptr)) == null) return -1;

    return (mstime_t)dictgetsignedintegerval(de);
}

总结

redis 的过期时间设计与实现包括以下几个关键点:

  • 设置过期时间:通过 expire、pexpire 等命令设置键的过期时间,并将过期时间存储在 expires 字典中。

  • 过期字典:每个数据库实例都有一个 expires 字典,用于存储键的过期时间。

  • 删除策略

    • 惰性删除:每次访问键时检查其是否过期,如果已过期则删除。
    • 定期删除:通过后台任务周期性地检测并删除过期键。
    • 主动删除:在内存使用接近最大限制时触发,扫描所有键并删除过期键。

定期删除activeexpirecycle函数详细解析

void activeexpirecycle(int type) {
    static unsigned int current_db = 0;  // 记录上一次处理的数据库索引
    static int timelimit_exit = 0;       // 用于指示是否超出时间限制
    unsigned int j;
    // 每次要处理的数据库数量
    unsigned int dbs_per_call = cron_dbs_per_call;
    long long start = ustime();          // 开始时间
    long long timelimit;                 // 时间限制

    if (type == active_expire_cycle_fast) {
        /* fast cycle: 1 ms */
        timelimit = 1000;
    } else {
        /* slow cycle: 25% cpu time p. db / configurable percentage. */
        timelimit = server.hz < 100 ? 1000 : 10;
        if (server.active_expire_effort != 1)
            timelimit *= server.active_expire_effort-1;
        timelimit /= server.dbnum;
        timelimit_exit = 0;
    }

    for (j = 0; j < dbs_per_call; j++) {
        redisdb *db = server.db + (current_db % server.dbnum);
        current_db++;
        int expired, sampled;

        do {
            long now = mstime();
            expireentry *de;
            dictentry *d;

            /* sample a few keys in the database */
            expired = 0;
            sampled = 0;
            while ((de = dictgetrandomkey(db->expires)) != null &&
                   mstime() - now < timelimit) {
                long long ttl = dictgetsignedintegerval(de) - mstime();
                if (ttl < 0) {
                    d = dictfind(db->dict, dictgetkey(de));
                    dbdelete(db, dictgetkey(d));
                    server.stat_expiredkeys++;
                    expired++;
                }
                sampled++;
            }
        } while (expired > active_expire_cycle_lookups_per_loop / 2);

        elapsed = ustime() - start;
        if (elapsed > timelimit) {
            timelimit_exit = 1;
            break;
        }
    }
}

关键步骤解析

1. 初始化变量

static unsigned int current_db = 0;
static int timelimit_exit = 0;
unsigned int j;
unsigned int dbs_per_call = cron_dbs_per_call;
long long start = ustime();
long long timelimit;
  • current_db:静态变量,用于记录上一次处理的数据库索引。
  • timelimit_exit:用于指示是否耗尽了时间配额,防止无限循环。
  • dbs_per_call:每次扫描的数据库数量,通常由配置决定。
  • start:记录开始执行此函数的时间戳。
  • timelimit:本次调用允许消耗的最大时间(以微秒为单位)。

2. 确定时间限制

if (type == active_expire_cycle_fast) {
    timelimit = 1000;  // 快速模式:1 毫秒
} else {
    timelimit = server.hz < 100 ? 1000 : 10;
    if (server.active_expire_effort != 1)
        timelimit *= server.active_expire_effort - 1;
    timelimit /= server.dbnum;
    timelimit_exit = 0;
}
  • 如果是快速模式,时间限制为 1 毫秒。
  • 如果是慢速模式,时间限制根据 redis 配置和当前服务器负载情况计算。
  • server.active_expire_effort 参数可以调整过期键清理的力度。

3. 遍历数据库

每次调用 activeexpirecycle 时,会遍历一定数量的数据库,并在每个数据库中随机抽取键进行过期检查和删除。

for (j = 0; j < dbs_per_call; j++) {
    redisdb *db = server.db + (current_db % server.dbnum);
    current_db++;
    int expired, sampled;

    do {
        long now = mstime();
        expireentry *de;
        dictentry *d;

        expired = 0;
        sampled = 0;
        while ((de = dictgetrandomkey(db->expires)) != null &&
               mstime() - now < timelimit) {
            long long ttl = dictgetsignedintegerval(de) - mstime();
            if (ttl < 0) {
                d = dictfind(db->dict, dictgetkey(de));
                dbdelete(db, dictgetkey(d));
                server.stat_expiredkeys++;
                expired++;
            }
            sampled++;
        }
    } while (expired > active_expire_cycle_lookups_per_loop / 2);

    elapsed = ustime() - start;
    if (elapsed > timelimit) {
        timelimit_exit = 1;
        break;
    }
}

关键点解析:

选择数据库

redisdb *db = server.db + (current_db % server.dbnum);
current_db++;

使用循环方式选择下一个要检查的数据库实例。current_db 记录上一次处理的数据库索引,通过取模操作确保索引在有效范围内。

初始化变量

int expired, sampled;
expired = 0;
sampled = 0;

过期检查循环

while ((de = dictgetrandomkey(db->expires)) != null && mstime() - now < timelimit) {
    long long ttl = dictgetsignedintegerval(de) - mstime();
    if (ttl < 0) {
        d = dictfind(db->dict, dictgetkey(de));
        dbdelete(db, dictgetkey(d));
        server.stat_expiredkeys++;
        expired++;
    }
    sampled++;
}
    • 从 expires 字典中随机获取一个键 de
    • 检查当前时间是否超过了本次周期的时间限制 timelimit
    • 如果键已经过期(ttl < 0),则删除该键,并增加已过期键的计数器 expired
    • 增加已检查键的计数器 sampled

多轮过期检查

do {
    ...
} while (expired > active_expire_cycle_lookups_per_loop / 2);
  • 如果在一轮检查中删除的过期键数量超过预设值的一半,则继续下一轮检查。

4. 时间限制检查

在每次处理完一个数据库后,检查是否超出时间限制:

elapsed = ustime() - start;
if (elapsed > timelimit) {
    timelimit_exit = 1;
    break;
}
  • 计算已耗费的时间 elapsed
  • 如果已耗费时间超过 timelimit,设置 timelimit_exit 为 1 并跳出循环。

总结

redis 的 activeexpirecycle 函数通过以下步骤实现定期删除过期键:

  • 初始化变量并确定时间限制:根据当前的模式(快速或慢速)和配置参数,计算本次函数调用的时间限制。
  • 遍历数据库:循环遍历一定数量的数据库。
  • 过期检查与删除:从每个数据库中随机抽取键,检查其是否过期并进行删除,直到达到时间限制或删除了一定数量的过期键。
  • 时间限制检查:确保函数不会超出规定的时间配额,以避免影响 redis 的其他操作。

aof、rdb和复制功能对过期键的处理

在 redis 中,aof(append only file)、rdb(redis database)和复制(replication)功能对过期键的处理方式有所不同。下面详细介绍这些机制如何处理过期键:

aof 持久化

  • 记录过期时间

    • 在 aof 文件中,除了写入每个键值的设置操作外,还会写入 expire 或 pexpire 命令来记录键的过期时间。
  • 重写(rewrite)过程

    • 当 aof 文件需要重写时,redis 会检查每个键,如果键已过期,则不会将其写入新的 aof 文件。
  • 加载 aof 文件

    • 当 redis 重启并加载 aof 文件时,会执行文件中的所有命令,包括设置键值和设置过期时间的命令。如果某些键已经过期,这些键会立即被删除。

rdb 持久化

  • 保存快照

    • 当 redis 创建 rdb 快照时,它会将所有键及其剩余的过期时间一起保存到快照文件中。
  • 加载快照

    • 当 redis 从 rdb 文件恢复数据时,会载入所有键值对,同时载入它们的过期时间。如果某个键在载入时已经过期,redis 会立即将其删除。

复制(replication)

  • 主从同步

    • 在主从复制架构中,主节点会将过期键的删除操作传播给从节点。
    • 如果一个键在主节点上过期并被删除,主节点会向从节点发送 del 操作,从而在从节点上也删除该键。
  • 延迟过期

    • 从节点可能因为网络延迟等原因,对过期键的处理会稍有滞后,但最终主从节点的数据将保持一致。

总结

  • aof:

    • 通过记录 expire/pexpire 命令来处理过期时间。
    • 在重写过程中跳过已过期的键。
    • 加载时删除已过期的键。
  • rdb:

    • 将过期时间与键值一起保存。
    • 加载时立即删除已过期的键。
  • 复制:

    • 主节点将删除过期键的操作同步到从节点。
    • 确保主从节点数据的一致性。

以上就是redis过期时间的设计与实现代码的详细内容,更多关于redis过期时间的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com