• 沒有找到結果。

过期键的惰性删除策略

4.4 慢查询日志

5.1.10 过期键的惰性删除策略

实现过期键惰性删除策略的核心是 db.c/expireIfNeeded 函数——所有命令在读取或写入数 据库之前,程序都会调用 expireIfNeeded 对输入键进行检查,并将过期键删除:

SET 、 LPUSH 、 SADD 、

等等

调用 expire_if_needed() 删除过期键

写请求

GET 、 LRANGE 、 SMEMBERS 、

等等 读请求

执行实际的命令流程

比如说,GET 命令的执行流程可以用下图来表示:

120 Chapter 5. 内部运作机制

Redis 设计与实现, 第一版

GET key

调用

expire_if_needed() 如果键已经过期

那么将它删除

key 不存在 向客户端返回 NIL

已过期

向客户端返回 key 的值 未过期

expireIfNeeded 的作用是,如果输入键已经过期的话,那么将键、键的值、键保存在 expires 字典中的过期时间都删除掉。

用伪代码描述的 expireIfNeeded 定义如下:

def expireIfNeeded(key):

# 对过期键执行以下操作 。。 if key.is_expired():

# 从键空间中删除键值对 db.dict.remove(key)

# 删除键的过期时间 db.expires.remove(key)

# 将删除命令传播到 AOF 文件和附属节点 propagateDelKeyToAofAndReplication(key)

5.1.11 过期键的定期删除策略

对过期键的定期删除由 redis.c/activeExpireCycle 函执行:每当 Redis 的例行处理程序 serverCron 执行时,activeExpireCycle 都会被调用——这个函数在规定的时间限制内,尽 可能地遍历各个数据库的 expires 字典,随机地检查一部分键的过期时间,并删除其中的过期 键。

整个过程可以用伪代码描述如下:

def activeExpireCycle():

# 遍历数据库(不一定能全部都遍历完,看时间是否足够)

for db in server.db:

# MAX_KEY_PER_DB 是一个 DB 最大能处理的 key 个数

# 它保证时间不会全部用在个别的 DB 上(避免饥饿)

i = 0

while (i < MAX_KEY_PER_DB):

# 数据库为空,跳出 while ,处理下个 DB if db.is_empty(): break

# 随机取出一个带 TTL 的键

key_with_ttl = db.expires.get_random_key()

# 检查键是否过期,如果是的话,将它删除 if is_expired(key_with_ttl):

db.deleteExpiredKey(key_with_ttl)

# 当执行时间到达上限,函数就返回,不再继续

# 这确保删除操作不会占用太多的 CPU 时间 if reach_time_limit(): return

i += 1

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

前面的内容讨论了过期键对 CPU 时间和内存的影响,现在,是时候说说过期键在 RDB 文件、

AOF 文件、AOF 重写以及复制中的影响了:

过期键会被保存在更新后的 RDB 文件、AOF 文件或者重写后的 AOF 文件里面吗?

附属节点会会如何处理过期键?处理的方式和主节点一样吗?

以上这些问题就是本节要解答的。

更新后的 RDB 文件

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

因此,过期键对更新后的 RDB 文件没有影响。

122 Chapter 5. 内部运作机制

Redis 设计与实现, 第一版

AOF 文件

在键已经过期,但是还没有被惰性删除或者定期删除之前,这个键不会产生任何影响,AOF 文 件也不会因为这个键而被修改。

当过期键被惰性删除、或者定期删除之后,程序会向 AOF 文件追加一条 DEL 命令,来显式地 记录该键已被删除。

举个例子,如果客户端使用 GET message 试图访问 message 键的值,但 message 已经过期了,

那么服务器执行以下三个动作:

1. 从数据库中删除 message ;

2. 追加一条 DEL message 命令到 AOF 文件;

3. 向客户端返回 NIL 。

AOF 重写

和 RDB 文件类似,当进行 AOF 重写时,程序会对键进行检查,过期的键不会被保存到重写 后的 AOF 文件。

因此,过期键对重写后的 AOF 文件没有影响。

复制

当服务器带有附属节点时,过期键的删除由主节点统一控制:

• 如果服务器是主节点,那么它在删除一个过期键之后,会显式地向所有附属节点发送一 个 DEL 命令。

• 如果服务器是附属节点,那么当它碰到一个过期键的时候,它会向程序返回键已过期的 回复,但并不真正的删除过期键。因为程序只根据键是否已经过期、而不是键是否已经被 删除来决定执行流程,所以这种处理并不影响命令的正确执行结果。当接到从主节点发来 的 DEL 命令之后,附属节点才会真正的将过期键删除掉。

附属节点不自主对键进行删除是为了和主节点的数据保持绝对一致,因为这个原因,当一个过 期键还存在于主节点时,这个键在所有附属节点的副本也不会被删除。

这种处理机制对那些使用大量附属节点,并且带有大量过期键的应用来说,可能会造成一部分 内存不能立即被释放,但是,因为过期键通常很快会被主节点发现并删除,所以这实际上也算 不上什么大问题。

5.1.13 数据库空间的收缩和扩展

因为数据库空间是由字典来实现的,所以数据库空间的扩展/收缩规则和字典的扩展/收缩规则 完全一样,具体的信息可以参考《字典》章节。

因 为 对 字 典 进 行 收 缩 的 时 机 是 由 使 用 字 典 的 程 序 决 定 的, 所 以 Redis 使 用 redis.c/tryResizeHashTables 函数来检查数据库所使用的字典是否需要进行收缩:每次 redis.c/serverCron 函数运行的时候,这个函数都会被调用。

/*

* 对服务器中的所有数据库键空间字典、以及过期时间字典进行检查,

* 看是否需要对这些字典进行收缩。

*

* 如果字典的使用空间比率低于 REDIS_HT_MINFILL

* 那么将字典的大小缩小,让 USED/BUCKETS 的比率 <= 1

*/

void tryResizeHashTables(void) { int j;

for (j = 0; j < server.dbnum; j++) { // 缩小键空间字典

if (htNeedsResize(server.db[j].dict)) dictResize(server.db[j].dict);

// 缩小过期时间字典

if (htNeedsResize(server.db[j].expires)) dictResize(server.db[j].expires);

} }

5.1.14 小结

• 数据库主要由 dict 和 expires 两个字典构成,其中 dict 保存键值对,而 expires 则 保存键的过期时间。

• 数据库的键总是一个字符串对象,而值可以是任意一种 Redis 数据类型,包括字符串、哈 希、集合、列表和有序集。

• expires 的某个键和 dict 的某个键共同指向同一个字符串对象,而 expires 键的值则 是该键以毫秒计算的 UNIX 过期时间戳。

• Redis 使用惰性删除和定期删除两种策略来删除过期的键。

• 更新后的 RDB 文件和重写后的 AOF 文件都不会保留已经过期的键。

• 当一个过期键被删除之后,程序会追加一条新的 DEL 命令到现有 AOF 文件末尾。

• 当主节点删除一个过期键之后,它会显式地发送一条 DEL 命令到所有附属节点。

• 附属节点即使发现过期键,也不会自作主张地删除它,而是等待主节点发来 DEL 命令,

这样可以保证主节点和附属节点的数据总是一致的。

• 数据库的 dict 字典和 expires 字典的扩展策略和普通字典一样。它们的收缩策略是:当 节点的填充百分比不足 10% 时,将可用节点数量减少至大于等于当前已用节点数量。

5.2 RDB

在运行情况下,Redis 以数据结构的形式将数据维持在内存中,为了让这些数据在 Redis 重启 之后仍然可用,Redis 分别提供了 RDB 和 AOF 两种持久化模式。

在 Redis 运行时,RDB 程序将当前内存中的数据库快照保存到磁盘文件中,在 Redis 重启动 时,RDB 程序可以通过载入 RDB 文件来还原数据库的状态。

124 Chapter 5. 内部运作机制

Redis 设计与实现, 第一版

RDB 功能最核心的是 rdbSave 和 rdbLoad 两个函数,前者用于生成 RDB 文件到磁盘,而后 者则用于将 RDB 文件中的数据重新载入到内存中:

内存中的 数据对象

磁盘中的 RDB 文件 rdbSave

rdbLoad

本章先介绍SAVEBGSAVE 命令的实现,以及 rdbSave 和 rdbLoad 两个函数的运行机制,

然后以图表的方式,分部分来介绍 RDB 文件的组织形式。

因为本章涉及 RDB 运行的相关机制,如果还没了解过 RDB 功能的话,请先阅读 Redis 官网 上的 persistence 手册 。