- 作者:老汪软件技巧
- 发表时间:2024-08-25 15:11
- 浏览量:
在 Redis 中,键的过期时间设计与实现是一个重要的功能,这使得 Redis 可以自动删除在指定时间后不再需要的键。下面详细介绍 Redis 过期时间的设计和实现,包括设置过期时间、过期键的存储结构、过期键的删除策略等。
1. 设置过期时间
Redis 提供了多个命令来设置键的过期时间,如 EXPIRE、PEXPIRE、EXPIREAT 和 PEXPIREAT。这些命令可以以秒或毫秒为单位设置键的过期时间,也可以设置具体的过期时间点。
示例:
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;
}
}
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;
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;
}
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;
}
}
关键点解析:
多轮过期检查:
do {
...
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP / 2);
如果在一轮检查中删除的过期键数量超过预设值的一半,则继续下一轮检查。
4. 时间限制检查
在每次处理完一个数据库后,检查是否超出时间限制:
elapsed = ustime() - start;
if (elapsed > timelimit) {
timelimit_exit = 1;
break;
}
总结
Redis 的 activeExpireCycle 函数通过以下步骤实现定期删除过期键:
初始化变量并确定时间限制:根据当前的模式(快速或慢速)和配置参数,计算本次函数调用的时间限制。遍历数据库:循环遍历一定数量的数据库。过期检查与删除:从每个数据库中随机抽取键,检查其是否过期并进行删除,直到达到时间限制或删除了一定数量的过期键。时间限制检查:确保函数不会超出规定的时间配额,以避免影响 Redis 的其他操作。AOF、RDB和复制功能对过期键的处理
在 Redis 中,AOF(Append Only File)、RDB(Redis DataBase)和复制(Replication)功能对过期键的处理方式有所不同。下面详细介绍这些机制如何处理过期键:
AOF 持久化
重写(Rewrite)过程:
加载 AOF 文件:
RDB 持久化
加载快照:
复制(Replication)
延迟过期:
总结
AOF:
RDB:
复制: