事件队列是一个在计算机编程中用来以异步的、非阻塞的方式管理和处理事件的数据结构。它通常用于事件驱动编程中,在此应用中,程序通过执行一系列称为事件处理器的代码来响应用户输入或系统事件。
当一个事件发生时,比如按钮点击或网络请求完成,它将被添加到事件队列中,同时包括任何关联的数据。然后,事件队列按接收顺序处理事件,通常是为每个事件调用适当的事件处理器函数。
通过使用事件队列,应用程序能够同时处理多个事件而不会阻塞主执行线程。这可以更有效地使用系统资源,并且能够提高应用程序的响应速度。
事件队列可以使用各种数据结构实现,例如链表或优先级队列,具体取决于应用程序的特定要求。它们用于多种类型的应用程序,包括图形用户界面、Web 服务器和视频游戏。
Redis 列表是一个有序字符串列表,与您可能熟悉的链表非常相似。在列表中新增一个值即为推送操作,从列表中移除一个值(从左边/头部/开头或右边/尾部/结尾)即为弹出操作,这些操作都非常轻量。正如您所想象的那样,这是一种非常适合管理队列的结构:将各项添加到列表的头,然后从尾部读取各项,以形成先进先出 (FIFO) 队列。
Redis 还提供了其他功能,这些功能让此类模式更加高效、可靠且易于使用。列表命令的一个子集允许采用“阻塞”行为。“阻塞”一词仅适用于连接的单个客户端——实际上,这些命令停止客户端执行任何操作,直到列表出现一个值(或超时)。这消除了对 Redis 进行结果轮询的需要。由于客户端在等待值时无法执行任何操作,因此我们需要两个开放的客户端来对此进行说明。
R | 发送客户端 | 阻塞客户端 |
---|---|---|
1 | > BRPOP my-q 0 [等待] | |
2 | > LPUSH my-q hello (整数) 1 | 1) “my-q” 2) “hello” [准备执行命令] |
3 | > BRPOP my-q 0 [等待] |
在此示例中的第 1 行,我们看到阻塞客户端未立即返回任何内容,因为列表(“my-q”)中没有任何值。最后一个参数是超时时间——在本例中,0 表示它永远不会超时,将一直等待下去。在第二行,发送客户端向 my-q 键发出 LPUSH,另一个客户端则立即结束阻塞。在第三行,我们可以发出另一个 BRPOP 命令(在客户端语言中通常借助循环完成),并等待任何进一步的列表值。您可以使用 ctrl-c 在 redis-cli 中退出阻塞。
让我们反转此示例,了解 BRPOP 如何与非空列表配合使用
R | 发送客户端 | 阻塞客户端 |
---|---|---|
1 | > LPUSH my-q hello (整数) 1 | |
2 | > LPUSH my-q hej (整数) 2 | |
3 | > LPUSH my-q bonjour (整数) 3 | |
4 | > BRPOP my-q 0 1) “my-q” 2) “hello” | |
5 | > BRPOP my-q 0 1) “my-q” 2) “hej” | |
6 | > BRPOP my-q 0 1) “my-q” 2) “bonjour” | |
7 | > BRPOP my-q 0 [等待] |
在第 1-3 行中,我们向列表中推入了三个值,我们能够看到响应不断增长(表示列表中的项目数量)。第 4 行在发出 BRPOP 命令后仍然立即返回了值?这是什么原因?因为仅当队列中没有项目时才会应用阻塞行为。在第 5-6 行中,我们可以看到同样的立即响应,因为它正在遍历队列中的每个项目。在第 7 行中,BRPOP 遇到一个空队列,并且在添加项目到队列之前,它会进行阻塞。
队列经常表示需要在另一个进程(一个工作进程)中完成某种类型的作业。在这类工作负载中,至关重要的是,如果一个工作进程在处理期间出现故障并出于某种原因而死亡,则作业不会丢失。Redis 也能支持这类队列。可以将 BRPOP 替换为 BRPOPLPUSH,而不用 BRPOP。BRPOPLPUSH(好拗口)会等待一个列表中的值,并在获得一个值之后,将其推送到另一个列表。所有这一切都是原子性完成的,因此,两个工作进程不可能移除/获取相同的值。让我们看看它如何工作的
R | 发送客户端 | 阻塞客户端 |
---|---|---|
1 | > LINDEX worker-q 0 (nil) | |
2 | [如果第 1 行的结果不是 nil,则使用它进行一些操作,否则,跳转到 4] | |
3 | > LREM worker-q -1 [1 中的值] (integer) 1 [循环回到 1] | |
4 | > BRPOPLPUSH my-q worker-q 0 [等待] | |
5 | > LPUSH my-q hello | “hello”(准备好接收命令) |
6 | [对“hello”进行一些操作] | |
7 | > LREM worker-q -1 hello (integer) 1 | |
8 | [循环回到第 1 行] |
在第 1 和第 2 行中,我们还没有进行任何操作,因为 worker-q 为空。如果 worker-q 中有东西出来,我们将会处理它并将其移除,然后跳回到 1 以查看队列中是否还有其他内容。这样,我们首先会清除工作队列,并执行已有的所有作业。在第 4 行中,我们等待一个值被添加到 my-q,并且在获取一个值之后,它会以原子性方式添加到 worker-q。接下来,我们对“hello”执行某种类型的非 Redis 操作,完成后,我们使用 LREM 从 worker-q 中移除一个实例,然后循环回到第 1 行。
真正关键的是,如果进程在第 6 行的操作期间死亡,则我们仍然有这个作业在 worker-q 中。在进程重新启动后,我们会立即清除第 7 行未移除的任何作业。此模式极大地降低了作业丢失的可能性。但是,一个作业有可能被处理两次,但仅当工作进程在第 2 和第 3 行或第 5 和第 6 行“之间”死亡时,这种情况不太可能发生,但最好在工作进程逻辑中考虑这种情况。