开发者

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

目录
  • 1. 设置过期时间
    • 示例:
  • 2. 过期键的存储结构
    • 3. 设置过期时间
      • 4. 删除过期键的策略
        • 5. 检查并删除过期键
          • 6. 获取过期时间
            • 总结
              • 定期删除activeExpireCycle函数详细解析
                • 关键步骤解析
                  • 1. 初始化变量
                  • 2. 确定时间限制
                  • 3. 遍历数据库
                  • 关键点解析:
                  • 4. 时间限制检查
                • 总结
                • AOF、RDB和复制功能对过期键的处理
                  • AOF 持久化
                    • RDB 持久化
                      • 复制(Replication)
                        • 总结

                        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 elaps编程客栈ed > 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 (androiddictSize(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)
                                    timelimitUKzOiXthNj *= 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 函数通过以下步骤实现定期删除过期键:

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

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

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

                        以上就是Redis过期时间的设计与实现代码的详细内容,更多关于Redis过期时间的资料请关注编程客栈(www.devze.com)其它相关文章!

                        0

                        上一篇:

                        下一篇:

                        精彩评论

                        暂无评论...
                        验证码 换一张
                        取 消

                        最新数据库

                        数据库排行榜