1. 设置过期时间
redis 提供了多个命令来设置键的过期时间,如 expire
、pexpire
、expireat
和 pexpireat
。这些命令可以以秒或毫秒为单位设置键的过期时间,也可以设置具体的过期时间点。
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
命令来记录键的过期时间。
- 在 aof 文件中,除了写入每个键值的设置操作外,还会写入
重写(rewrite)过程:
- 当 aof 文件需要重写时,redis 会检查每个键,如果键已过期,则不会将其写入新的 aof 文件。
加载 aof 文件:
- 当 redis 重启并加载 aof 文件时,会执行文件中的所有命令,包括设置键值和设置过期时间的命令。如果某些键已经过期,这些键会立即被删除。
rdb 持久化
保存快照:
- 当 redis 创建 rdb 快照时,它会将所有键及其剩余的过期时间一起保存到快照文件中。
加载快照:
- 当 redis 从 rdb 文件恢复数据时,会载入所有键值对,同时载入它们的过期时间。如果某个键在载入时已经过期,redis 会立即将其删除。
复制(replication)
主从同步:
- 在主从复制架构中,主节点会将过期键的删除操作传播给从节点。
- 如果一个键在主节点上过期并被删除,主节点会向从节点发送
del
操作,从而在从节点上也删除该键。
延迟过期:
- 从节点可能因为网络延迟等原因,对过期键的处理会稍有滞后,但最终主从节点的数据将保持一致。
总结
aof:
- 通过记录
expire
/pexpire
命令来处理过期时间。 - 在重写过程中跳过已过期的键。
- 加载时删除已过期的键。
- 通过记录
rdb:
- 将过期时间与键值一起保存。
- 加载时立即删除已过期的键。
复制:
- 主节点将删除过期键的操作同步到从节点。
- 确保主从节点数据的一致性。
以上就是redis过期时间的设计与实现代码的详细内容,更多关于redis过期时间的资料请关注代码网其它相关文章!
发表评论