事件队列是计算机编程中使用的一种数据结构,用于以异步、非阻塞的方式管理和处理事件。 它通常用于事件驱动编程,其中应用程序通过执行一段称为事件处理程序的代码来响应用户输入或系统事件。
当事件发生时(例如按钮单击或网络请求完成),它会连同任何相关数据一起添加到事件队列。 然后,事件队列按照接收到的顺序处理事件,通常通过为每个事件调用相应的事件处理函数。
通过使用事件队列,应用程序可以同时处理多个事件,而不会阻塞主执行线程。 这可以更有效地利用系统资源,并可以提高应用程序的响应速度。
事件队列可以使用各种数据结构来实现,例如链表或优先级队列,具体取决于应用程序的特定要求。 它们用于许多类型的应用程序,包括图形用户界面、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”) 中没有任何值。 最后一个参数是超时——在本例中,零意味着它永远不会超时并将永远等待。 第二行,发送客户端向 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 遇到一个空队列并阻塞,直到项目添加到队列中。
队列通常代表需要在另一个进程(worker)中完成的某种作业。 在这种类型的工作负载中,至关重要的是,如果 worker 发生故障并因某种原因在处理过程中死亡,则作业不会丢失。 Redis 也可以支持这种类型的队列。 不要使用 BRPOP,而是替换为 BRPOPLPUSH。 BRPOPLPUSH(多么拗口)等待一个列表中的值,一旦获得一个值,它就会将其推送到另一个列表。 这一切都是原子完成的,因此两个 worker 不可能删除/获取相同的值。 让我们看看它是如何工作的
R | 发送客户端 | 阻塞客户端 |
---|---|---|
1 | > LINDEX worker-q 0 (nil) | |
2 | [如果第 1 行的结果不是 nil,则使用它执行某些操作,否则跳转到 4] | |
3 | > LREM worker-q -1 [来自 1 的值] (整数) 1 [循环回到 1] | |
4 | > BRPOPLPUSH my-q worker-q 0 [等待] | |
5 | > LPUSH my-q hello | “hello” [准备好接收命令] |
6 | [使用“hello”做一些事情] | |
7 | > LREM worker-q -1 hello (整数) 1 | |
8 | [循环回到第 1 行] |
在第 1 行和第 2 行中,由于 worker-q 为空,我们目前没有做任何事情。 如果有东西从 worker-q 中出来,我们会处理它并将其删除,然后跳回 1 以查看队列中是否还有其他内容。 这样,我们首先清除 worker 队列,并执行已经存在的任何作业。 在第 4 行中,我们等待直到将一个值添加到 my-q,当我们获得一个值时,它会被原子地添加到 worker-q。 接下来,我们对“hello”执行某种非 Redis 操作,当我们完成时,我们使用 LREM 从 worker-q 中删除一个实例并循环回到第 1 行。
真正的关键是,如果进程在第 6 行的操作期间死亡,我们仍然在 worker-q 中有这个作业。 进程重新启动后,我们将立即清除第 7 行尚未删除的任何作业。 这种模式大大降低了作业丢失的可能性。 但是,作业可能会被处理两次,但仅当 worker 在第 2 行和第 3 行之间或第 5 行和第 6 行之间“死亡”时,这种情况不太可能发生,但最好是在您的 worker 逻辑中考虑到这种情况。