Redis 「5」事件处理模型与键过期策略

226 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

01-Redis 单线程模型

Redis 服务端是基于 Reactor 模式实现的事件驱动程序,并需要处理两类的事件:

  • 文件事件,简单来讲就是 Redis 客户端通过 Socket 发来的命令。
  • 时间事件,服务端中需要特定时间点执行的操作,例如serverCron函数。

Redis 的单线程模型实际指的是上述两类事件是在同一个线程内处理的。但是为了能同时处理多个客户端的连接,Redis 中用到了 IO 多路复用技术。《Redis 设计与实现》中对 Redis 中的多路复用介绍如下:

文件事件处理器以单线程方式运行,但通过 IO 多路复用程序来监听多个客户端套接字。既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部线程模型的简单性。

Redis 中单线程的事件循环可以用如下伪代码表示:

def eventLoop():
    while True:
	# 处理文件事件,接受明令以及发送命令回复
	processFileEvents()
	# 处理时间事件
	processTimeEvents()
	# 其他操作
	...

01.1-文件事件处理器

IO 多路复用模型

  • 当 Socket 变得可读时,会产生AE_READABLE事件
  • 当 Socket 变得可写时,会产生AE_WRITABLE事件

一次完整的客户端与服务端连接过程中,事件及处理器的过程如下:

  1. 服务端监听套接字AE_READABLE事件,且此时该事件对应的处理器为连接应答处理器;
  2. 当客户端发起连接后,监听套接字将产生AE_READABLE事件,连接应答处理器负责创建客户端套接字,并将客户端套接字的AE_READABLE事件与命令请求处理器绑定;
  3. 当客户端发送命令时,客户端套接字将发生AE_READABLE事件,命令请求处理器负责处理这些命令,并产生回复。
  4. 为了将命令回复传送到客户端,服务器会将客户端套接字的AE_WRITABLE事件与命令回复处理器绑定。
  5. 当客户端尝试读取命令回复时,会触发AE_WRITABLE事件,命令回复处理器会将回复写入套接字。之后,服务器会解除AE_WRITABLE事件与命令回复处理器的绑定。

01.2-时间事件处理器

Redis 中时间事件分为2类:

  • 定时事件,例如 30ms 后执行一次
  • 周期事件,例如每隔 30ms 执行一次

每隔时间事件,都包含如下三个要素:

  1. id,全局唯一 ID,且按照事件先后顺序递增。
  2. when,事件到达时间,毫秒精度 UNIX 时间戳。
  3. timeProc,事件处理器,用于处理响应的事件。其返回值决定事件是定时的(ae.h/AE_NOMORE),还是周期性的。

[1] Reactor pattern (Node.js Netty 等知名项目的线程模型也都基于 Reactor 模式)

02-键过期

Redis 中设置键过期的方式有如下几种:

  • expire key ttl,ttl 秒后过期
  • pexpire key ttl,ttl 毫秒后过期
  • pexpireat key timestamp,在 timestamp 时刻过期

无论上述哪种方式,最终都通过pexpireat方式实现。过期的实现原理如下:在redisDb中维护了一个 dict(过期字典),记录了哪些键在何时应当过期。

typedef struct redisDb {
	...
	// 其键为 String 类型,其值为 long long类型
	dict *expires;
	...
}

过期判定的原则就是,将过期字典中的键对应的值与当前系统时间相比,若小于系统时间,则认为键过期。

02.1-键过期删除策略

一般来说,键过期删除策略有三种:

  • 定时删除,在设置键的过期时间时,启动一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除。优缺点:对内存最友好,但对 CPU 最不友好。
  • 惰性删除,过期时并不删除,但是每次从键空间获取键时,都判断键是否过期。若过期则删除,否则返回该键的值。对内存不友好,但对 CPU 友好。
  • 定期删除,每隔一段时间就对数据库进行检查,删除里面的过期键。(前两种方式的混合)。

02.2-Redis 内存淘汰机制

虽然 Redis 通常采用惰性删除和定期删除的混合策略,但仍有可能有大量过期的键驻留在内存中,导致 OOM。Redis 是如何解决这个问题的呢?你可能需要了解如下几种 Redis 的内存淘汰机制:

  1. volatile-lru,从server.db[i].expires中挑选最近最少使用的淘汰;
  2. volatile-ttl,从server.db[i].expires中挑选将要过期的数据淘汰;
  3. volatile-random, 从server.db[i].expires中随机挑选数据淘汰;
  4. allkeys-lru,从server.db[i].dict中挑选最近最少使用的淘汰;
  5. allkeys-random,从server.db[i].dict中随机挑选数据淘汰;
  6. no-eviction,如果内存不足以写入新数据,拒绝淘汰,写入报错;
  7. volatile-lfu,从server.db[i].expires中挑选最不经常使用的淘汰;
  8. allkeys-lfu,从server.db[i].dict中挑选最不经常使用的淘汰;

历史文章

Redis 「4」Redis 在秒杀系统中的应用

Redis 「3」持久化

Redis 「2」缓存一致性与异常处理

Redis 「1」流水线、事务、Lua 脚本