发布于 2015-09-17 14:55:28 | 177 次阅读 | 评论: 0 | 来源: 网络整理
事件是 Redis 服务器的核心, 它处理两项重要的任务:
本文以下内容就来介绍这两种事件, 以及它们背后的运作模式。
Redis 服务器通过在多个客户端之间进行多路复用, 从而实现高效的命令请求处理: 多个客户端通过套接字连接到 Redis 服务器中, 但只有在套接字可以无阻塞地进行读或者写时, 服务器才会和这些客户端进行交互。
Redis 将这类因为对套接字进行多路复用而产生的事件称为文件事件(file event), 文件事件可以分为读事件和写事件两类。
读事件标志着客户端命令请求的发送状态。
当一个新的客户端连接到服务器时, 服务器会给为该客户端绑定读事件, 直到客户端断开连接之后, 这个读事件才会被移除。
读事件在整个网络连接的生命期内, 都会在等待和就绪两种状态之间切换:
作为例子, 下图展示了三个已连接到服务器、但并没有发送命令的客户端:
这三个客户端的状态如下表:
客户端 | 读事件状态 | 命令发送状态 |
客户端 X | 等待 | 未发送 |
客户端 Y | 等待 | 未发送 |
客户端 Z | 等待 | 未发送 |
之后, 当客户端 X 向服务器发送命令请求, 并且命令请求已到达时, 客户端 X 的读事件状态变为就绪:
这时, 三个客户端的状态如下表(只有客户端 X 的状态被更新了):
客户端 | 读事件状态 | 命令发送状态 |
客户端 X | 就绪 | 已发送,并且已到达 |
客户端 Y | 等待 | 未发送 |
客户端 Z | 等待 | 未发送 |
当事件处理器被执行时, 就绪的文件事件会被识别到, 相应的命令请求会被发送到命令执行器, 并对命令进行求值。
写事件标志着客户端对命令结果的接收状态。
和客户端自始至终都关联着读事件不同, 服务器只会在有命令结果要传回给客户端时, 才会为客户端关联写事件, 并且在命令结果传送完毕之后, 客户端和写事件的关联就会被移除。
一个写事件会在两种状态之间切换:
当客户端向服务器发送命令请求, 并且请求被接受并执行之后, 服务器就需要将保存在缓存内的命令执行结果返回给客户端, 这时服务器就会为客户端关联写事件。
作为例子, 下图展示了三个连接到服务器的客户端, 其中服务器正等待客户端 X 变得可写, 从而将命令的执行结果返回给它:
此时三个客户端的事件状态分别如下表:
客户端 | 读事件状态 | 写事件状态 |
客户端 X | 等待 | 等待 |
客户端 Y | 等待 | 无 |
客户端 Z | 等待 | 无 |
当客户端 X 的套接字可以进行无阻塞写操作时, 写事件就绪, 服务器将保存在缓存内的命令执行结果返回给客户端:
此时三个客户端的事件状态分别如下表(只有客户端 X 的状态被更新了):
客户端 | 读事件状态 | 写事件状态 |
客户端 X | 等待 | 已就绪 |
客户端 Y | 等待 | 无 |
客户端 Z | 等待 | 无 |
当命令执行结果被传送回客户端之后, 客户端和写事件之间的关联会被解除(只剩下读事件), 至此, 返回命令执行结果的动作执行完毕:
Note
同时关联写事件和读事件
前面提到过,读事件只有在客户端断开和服务器的连接时,才会被移除。
这也就是说,当客户端关联写事件的时候,实际上它在同时关联读/写两种事件。
因为在同一次文件事件处理器的调用中, 单个客户端只能执行其中一种事件(要么读,要么写,但不能又读又写), 当出现读事件和写事件同时就绪的情况时, 事件处理器优先处理读事件。
这也就是说, 当服务器有命令结果要返回客户端, 而客户端又有新命令请求进入时, 服务器先处理新命令请求。
时间事件记录着那些要在指定时间点运行的事件, 多个时间事件以无序链表的形式保存在服务器状态中。
每个时间事件主要由三个属性组成:
when
:以毫秒格式的 UNIX 时间戳为单位,记录了应该在什么时间点执行事件处理函数。timeProc
:事件处理函数。next
指向下一个时间事件,形成链表。根据 timeProc
函数的返回值,可以将时间事件划分为两类:
ae.h/AE_NOMORE
,那么这个事件为单次执行事件:该事件会在指定的时间被处理一次,之后该事件就会被删除,不再执行。AE_NOMORE
的整数值,那么这个事件为循环执行事件:该事件会在指定的时间被处理,之后它会按照事件处理函数的返回值,更新事件的 when
属性,让这个事件在之后的某个时间点再次运行,并以这种方式一直更新并运行下去。可以用伪代码来表示这两种事件的处理方式:
def handle_time_event(server, time_event): # 执行事件处理器,并获取返回值 # 返回值可以是 AE_NOMORE ,或者一个表示毫秒数的非符整数值 retval = time_event.timeProc() if retval == AE_NOMORE: # 如果返回 AE_NOMORE ,那么将事件从链表中删除,不再执行 server.time_event_linked_list.delete(time_event) else: # 否则,更新事件的 when 属性 # 让它在当前时间之后的 retval 毫秒之后再次运行 time_event.when = unix_ts_in_ms() + retval
当时间事件处理器被执行时, 它遍历所有链表中的时间事件, 检查它们的到达事件(when
属性), 并执行其中的已到达事件:
def process_time_event(server): # 遍历时间事件链表 for time_event in server.time_event_linked_list: # 检查事件是否已经到达 if time_event.when <= unix_ts_in_ms(): # 处理已到达事件 handle_time_event(server, time_event)
Note
无序链表并不影响时间事件处理器的性能
在目前的版本中, 正常模式下的 Redis 只带有 serverCron
一个时间事件, 而在 benchmark 模式下, Redis 也只使用两个时间事件。
在这种情况下, 程序几乎是将无序链表退化成一个指针来使用, 所以使用无序链表来保存时间事件, 并不影响事件处理器的性能。
对于持续运行的服务器来说, 服务器需要定期对自身的资源和状态进行必要的检查和整理, 从而让服务器维持在一个健康稳定的状态, 这类操作被统称为常规操作(cron job)。
在 Redis 中, 常规操作由 redis.c/serverCron
实现, 它主要执行以下操作:
Redis 将 serverCron
作为时间事件来运行, 从而确保它每隔一段时间就会自动运行一次, 又因为 serverCron
需要在 Redis 服务器运行期间一直定期运行, 所以它是一个循环时间事件: serverCron
会一直定期执行,直到服务器关闭为止。
在 Redis 2.6 版本中, 程序规定 serverCron
每秒运行 10
次, 平均每 100
毫秒运行一次。 从 Redis 2.8 开始, 用户可以通过修改 hz
选项来调整 serverCron
的每秒执行次数, 具体信息请参考 redis.conf
文件中关于 hz
选项的说明。
既然 Redis 里面既有文件事件, 又有时间事件, 那么如何调度这两种事件就成了一个关键问题。
简单地说, Redis 里面的两种事件呈合作关系, 它们之间包含以下三种属性:
serverCron
)poll
函数的最大阻塞时间),由距离到达时间最短的时间事件决定。这些属性表明, 实际处理时间事件的时间, 通常会比时间事件所预定的时间要晚, 至于延迟的时间有多长, 取决于时间事件执行之前, 执行文件事件所消耗的时间。
比如说, 以下图表就展示了, 虽然时间事件 TE 1
预定在 t1
时间执行, 但因为文件事件 FE 1
正在运行, 所以 TE 1
的执行被延迟了:
t1 | V time -----------------+------------------->| | FE 1 | TE 1 | |<------>| TE 1 delay time
另外, 对于像 serverCron
这类循环执行的时间事件来说, 如果事件处理器的返回值是 t
, 那么 Redis 只保证:
t
, 那么这个时间事件至少会被处理一次。t
时间, 就一定要执行一次事件 —— 这对于不使用抢占调度的 Redis 事件处理器来说,也是不可能做到的举个例子, 虽然 serverCron
(sC
)设定的间隔为 10
毫秒, 但它并不是像如下那样每隔 10
毫秒就运行一次:
time ----------------------------------------------------->| |<---- 10 ms ---->|<---- 10 ms ---->|<---- 10 ms ---->| | FE 1 | FE 2 | sC 1 | FE 3 | sC 2 | FE 4 | ^ ^ ^ ^ ^ | | | | | file event time event | time event | handler handler | handler | run run | run | file event file event handler handler run run
在实际中, serverCron
的运行方式更可能是这样子的:
time ----------------------------------------------------------------------->| |<---- 10 ms ---->|<---- 10 ms ---->|<---- 10 ms ---->|<---- 10 ms ---->| | FE 1 | FE 2 | sC 1 | FE 3 | FE 4 | FE 5 | sC 2 | |<-------- 15 ms -------->| |<------- 12 ms ------->| >= 10 ms >= 10 ms ^ ^ ^ ^ | | | | file event time event | time event handler handler | handler run run | run file event handler run
根据情况, 如果处理文件事件耗费了非常多的时间, serverCron
被推迟到一两秒之后才能执行, 也是有可能的。
整个事件处理器程序可以用以下伪代码描述:
def process_event(): # 获取执行时间最接近现在的一个时间事件 te = get_nearest_time_event(server.time_event_linked_list) # 检查该事件的执行时间和现在时间之差 # 如果值 <= 0 ,那么说明至少有一个时间事件已到达 # 如果值 > 0 ,那么说明目前没有任何时间事件到达 nearest_te_remaind_ms = te.when - now_in_ms() if nearest_te_remaind_ms <= 0: # 如果有时间事件已经到达 # 那么调用不阻塞的文件事件等待函数 poll(timeout=None) else: # 如果时间事件还没到达 # 那么阻塞的最大时间不超过 te 的到达时间 poll(timeout=nearest_te_remaind_ms) # 处理已就绪文件事件 process_file_events() # 处理已到达时间事件 process_time_event()
通过这段代码, 可以清晰地看出:
poll
的最大阻塞时长。将这个事件处理函数置于一个循环中, 加上初始化和清理函数, 这就构成了 Redis 服务器的主函数调用:
def redis_main(): # 初始化服务器 init_server() # 一直处理事件,直到服务器关闭为止 while server_is_not_shutdown(): process_event() # 清理服务器 clean_server()
serverCron
就是循环事件。