Redis享知享学:单机数据库实现:expires属性

本篇继续解析单机数据库的实现,理解第二个属性expires。

Redis的过期删除采用了什么策略?过期键会被保存在更新后的RDB文件吗?主从节点对过期键的处理方式有什么不同?本篇将走进源码,寻找答案。

expires

该属性用于保存键的过期时间,Redis提供多种命令设置键的过期时间,但是最终都会将设置的数值通过相应函数转化为UNIX时间戳来保存,类型是long long。

这里涉及两方面内容,一个是过期判定,一个是过期删除。

过期判定

主要通过expireIfNeeded方法来判定,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int expireIfNeeded(redisDb *db, robj *key) {
// 取出 key 的过期时间
long long when = getExpire(db,key);
// key 没有过期时间,直接返回
if (when < 0) return 0; /* No expire for this key */
/* Don't expire anything while loading. It will be done later. */
// 不要在服务器载入数据时执行过期
if (server.loading) return 0;
...
/* Return when this key has not expired */
// 未过期
if (mstime() <= when) return 0;
/* Delete the key */
server.stat_expiredkeys++;
...
// 从数据库中删除 key
return dbDelete(db,key);
}

暂时隐去了涉及多节点的代码。

过期删除

过期删除的策略和之前分析字典实现时的rehash策略是类似的,都可以算是用存储开销来换取计算开销:实时的释放内存会增加CPU的计算负担。

所以Redis采用了称为惰性删除和定期删除的策略。

惰性删除策略

简单思路就是:键虽然过期了,但是不会立马删除它,而是在访问键的时候再处理。

也就是每次访问的时候调用expireIfNeeded方法,如果过期了就进行删除操作dbDelete()并返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Delete a key, value, and associated expiration entry if any, from the DB */
/*
* 从数据库中删除 key ,key 对应的值,以及对应的过期时间(如果有的话)
*/
int dbDelete(redisDb *db, robj *key) {
/* Deleting an entry from the expires dict will not free the sds of
* the key, because it is shared with the main dictionary. */
// 先删除过期时间
if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
// 删除 key 和 value
if (dictDelete(db->dict,key->ptr) == DICT_OK) {
if (server.cluster_enabled) SlotToKeyDel(key);
return 1;
} else {
return 0;
}
}

定期删除策略

简单思路就是:每隔一段时间执行一次删除。

该功能通过activeExpireCycle方法实现,在serverCron()内被调用:

1
2
3
4
5
6
/* Expire a few keys per cycle, only if this is a master.
* On slaves we wait for DEL operations synthesized by the master
* in order to guarantee a strict consistency. */
// 如果服务器是主节点的话,进行过期键删除
// 如果服务器是附属节点的话,那么等待主节点发来的 DEL 命令
if (server.masterhost == NULL) activeExpireCycle();

过期键对RDB、AOF和复制的影响

RDB:

创建RDB文件时,程序会对键进行检查,过期的键不会被写入到更新后的RDB文件中。

AOF:

触发惰性删除或定期删除事件,程序会向AOF文件追加DEL命令。

复制:

假设现在进行可能发生惰性删除(定期删除类似)的操作,那么会调用expireIfNeeded(),在前文列出该方法源码的时候,本人暂时隐去了涉及多节点的代码,这里显现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 第一个省略符号...下的代码:
/* If we are running in the context of a slave, return ASAP:
* the slave key expiration is controlled by the master that will
* send us synthesized DEL operations for expired keys.
*
* Still we try to return the right information to the caller,
* that is, 0 if we think the key should be still valid, 1 if
* we think the key is expired at this time. */
// 如果服务器作为附属节点运行,那么直接返回
// 因为附属节点的过期是由主节点通过发送 DEL 命令来删除的
// 不必自主删除
if (server.masterhost != NULL) {
// 返回一个理论上正确的值,但不执行实际的删除操作
return mstime() > when;
}
// 第二个省略符号...下的代码:
// 传播过期命令
propagateExpire(db,key);

server.masterhost != NULL为true,说明是从节点,返回mstime() > when的值,不会再进行后续删除的操作;

如果能执行到propagateExpire(db,key);,说明是主节点且键是过期键,会通知其它从节点也要删除这个键,并在后面的代码中主动调用方法删除键;

这些代码说明了在复制条件下,删除过期键的原则:

  • 从节点没有主动删除过期键的权利,应对客户端的请求,它只有“观察”的权利,返回的值可以告知客户端该键是否过期;
  • 主节点在确认键是过期节点,并将在数据库中删除该键之前,会传播过期命令,通知从节点也删掉该键;

参考

Redis 设计与实现:国内解析Redis的开源资料;