开始前,会这些就够了
- 认识请求与响应;能运行一个 Python 文件
遇到不熟悉的词,先看这 2 个解释
- 幂等
- 在约定的范围内,把同一个请求做一次或重复做多次,对目标业务产生的预期效果相同;它用来防止重复执行把一件事变成额外多办几次。
- 队列
- 把暂时不能立即处理的任务或消息先保存起来,等处理方取走;它把提交与执行分开,可以缓冲短时忙碌,但等待中的数量也会占用资源。
假设消息队列依次发出“建立订单 A”和“订单 A 已付款”。第一个消费者处理建单需要五个时间单位,第二个处理付款只要一个。即使消息确实按顺序被领取,付款处理仍可能先结束。
要讨论“有序”,先写出有序发生在哪一步、针对哪一组记录。只说队列先进先出,远远不够说明几个程序最终怎样修改业务数据。
收到的顺序与完成的顺序不是同一个队列
可以分别记录生产者发出、队列接收、消费者领取和业务提交四种顺序。消息系统可能对其中某段提供约定,但后续应用又开线程、调用外部接口或重试,就会加入新的等待。
例如 RabbitMQ 4.1 文档说明同一 channel 发布的消息按发布顺序入队,同时也指出多个消费者、优先级和重新投递会改变观察到的顺序。多个生产连接并发发布时,还不能凭各自本地日志拼出一条天然的全局顺序。
三个任务足够看见差别
程序把三个任务依次交给两个最早空闲的假设工人。工作时长由我们指定,都是虚拟单位;程序没有启动队列或线程。它只回答:即使领取顺序固定,按完成时间排序还会不会一样?
输出里的 pay:A 先结束,并不表示真实支付已经执行;这些只是任务名字。若业务确实要求建单生效后才能处理付款,就需要另外表达依赖,而不能指望快任务主动等慢任务。
jobs = [("create:A", 5), ("pay:A", 1), ("create:B", 1)]
available = [0, 0] # Two hypothetical workers, initially idle.
completed = []
for sequence, (event, duration) in enumerate(jobs):
worker = min(range(2), key=lambda i: (available[i], i))
start = available[worker]
end = start + duration
available[worker] = end
completed.append((end, sequence, event))
print("delivered:", [event for event, duration in jobs])
print("completed:", [event for end, sequence, event in sorted(completed)])
怎样运行:Python 3.10+ 标准库;保存为 message_completion.py,运行 python message_completion.py。
运行后对照结果
delivered: ['create:A', 'pay:A', 'create:B']
completed: ['pay:A', 'create:B', 'create:A']
很多业务只要求同一订单内部按顺序
订单 A 和订单 B 往往可以同时处理,但 A 的后续状态需要尊重 A 自己的先后。一种设计是把相同订单键交给同一条处理序列,在完成前一个相关动作后,再开始下一个;不同订单仍可使用不同序列。
Kafka 4.0 入门文档的顺序说明以一个 topic-partition,也就是主题中的一个分区为范围。按业务键稳定分配到同一分区有助于保持相关事件的读取顺序,却不意味着不同分区共用一条全局顺序;应用把读到的记录再并行处理,也仍要约束完成顺序。
失败重来时,要决定后来的事怎么办
假设 A 的建单处理失败,付款事件已经到了。可选择暂停 A 后续的处理,或把依赖未满足的事件记录为待处理,之后再检查。无论选哪种,都要限制积压并留出查询与处理入口;悄悄跳过失败事件,可能破坏本来承诺的顺序。
重复事件又是另一个问题。为每个订单记录已经接受的事件编号或版本,并把它与本地业务修改一起处理,可以帮助识别过旧或重复事件;但版本不连续、事件丢失和外部副作用仍需单独设计。这是待验证的应用方案,不是本文小程序已经实现的功能。
把承诺说到可以验证的程度
“同一订单、固定分配规则、单条顺序处理链、前一事件成功后推进”比“消息绝对有序”更容易检查。扩分区、重新分配任务、故障恢复或修改线程模型时,都应重新核对这些条件。
演练时保留消息身份、业务键、版本、领取时间和提交结果。只看消费者打印“收到”的一行,证明不了业务修改已按顺序生效。本篇没有运行 Kafka 或 RabbitMQ,实际产品配置仍须在相应版本环境中验证。
记住这三点
- 入队、领取和处理完成可能各有不同顺序。
- 先限定业务键与分区范围,再决定需要怎样的串行处理。
- 故障重试、重复事件和恢复都要继续遵守原来的顺序承诺。
别把例子用过头
- 分区内读取有序不能自动保证应用并行处理后的提交顺序。
- 同一键分配规则改变时,不能默认仍落在同一个处理范围。
- 虚拟任务模型没有验证真实队列的优先级、确认或重投配置。
自己试一小步
订单 A 的第二个事件先完成,订单 B 完全正常:怎样只限制 A 的顺序,而不让 B 跟着等待?
- 为每条任务加业务键和版本,画出哪些依赖是必要的。
- 设计按键推进的虚拟调度,再加入 A 第一个事件失败的情况。
- 记录恢复时是继续、重试还是等待缺失事件,不要只写“保证顺序”。
做到这些,就算完成
- 没有把所有订单都锁进同一个全局串行流程来回避问题。
- 能说明同一键的前一步何时算完成。
- 包含重复到达和缺少前序事件两种不同情况。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- RabbitMQ 4.1:Queues (新标签页)↗
Message Ordering:单 channel 入队顺序、多消费者、优先级和重新投递的边界。
- Apache Kafka 4.0:Introduction (新标签页)↗
主题、分区与事件键介绍:顺序保证以某个 topic-partition 的记录为范围。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
RocketMQ 中文:同一订单的消息怎样保持先后 (新标签页)↗
只读同组生产顺序与消费顺序,把建单、付款、发货分组画出,检查处理完成和确认之间有没有异步岔路。
查看学习准备与访问说明 →RabbitMQ 官方 Java 教程:任务队列与可靠发布 (新标签页)↗
只做两个消费者分担任务的练习,让一项任务处理得慢一些,分别记录领取次序和完成次序。
查看学习准备与访问说明 →