qinghuan
← 回到知识探索

后端现场 / 动手实验

消息按顺序发了,为什么付款处理跑到建单前面?

把入队、领取、完成和业务生效分开,找出多消费者顺序保证的边界。

展开本文目录
  1. 收到的顺序与完成的顺序不是同一个队列
  2. 三个任务足够看见差别
  3. 很多业务只要求同一订单内部按顺序
  4. 失败重来时,要决定后来的事怎么办
  5. 把承诺说到可以验证的程度
  6. 记住这三点
  7. 别把例子用过头
  8. 自己试一小步
  9. 查原始资料
  10. 找一份资料继续练
开始前,会这些就够了
  • 认识请求与响应;能运行一个 Python 文件
遇到不熟悉的词,先看这 2 个解释
幂等
在约定的范围内,把同一个请求做一次或重复做多次,对目标业务产生的预期效果相同;它用来防止重复执行把一件事变成额外多办几次。
队列
把暂时不能立即处理的任务或消息先保存起来,等处理方取走;它把提交与执行分开,可以缓冲短时忙碌,但等待中的数量也会占用资源。
打开完整术语小抄 ↗

假设消息队列依次发出“建立订单 A”和“订单 A 已付款”。第一个消费者处理建单需要五个时间单位,第二个处理付款只要一个。即使消息确实按顺序被领取,付款处理仍可能先结束。

要讨论“有序”,先写出有序发生在哪一步、针对哪一组记录。只说队列先进先出,远远不够说明几个程序最终怎样修改业务数据。

收到的顺序与完成的顺序不是同一个队列

可以分别记录生产者发出、队列接收、消费者领取和业务提交四种顺序。消息系统可能对其中某段提供约定,但后续应用又开线程、调用外部接口或重试,就会加入新的等待。

例如 RabbitMQ 4.1 文档说明同一 channel 发布的消息按发布顺序入队,同时也指出多个消费者、优先级和重新投递会改变观察到的顺序。多个生产连接并发发布时,还不能凭各自本地日志拼出一条天然的全局顺序。

三个任务足够看见差别

程序把三个任务依次交给两个最早空闲的假设工人。工作时长由我们指定,都是虚拟单位;程序没有启动队列或线程。它只回答:即使领取顺序固定,按完成时间排序还会不会一样?

输出里的 pay:A 先结束,并不表示真实支付已经执行;这些只是任务名字。若业务确实要求建单生效后才能处理付款,就需要另外表达依赖,而不能指望快任务主动等慢任务。

小实验 · python
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 跟着等待?

  1. 为每条任务加业务键和版本,画出哪些依赖是必要的。
  2. 设计按键推进的虚拟调度,再加入 A 第一个事件失败的情况。
  3. 记录恢复时是继续、重试还是等待缺失事件,不要只写“保证顺序”。

做到这些,就算完成

  • 没有把所有订单都锁进同一个全局串行流程来回避问题。
  • 能说明同一键的前一步何时算完成。
  • 包含重复到达和缺少前序事件两种不同情况。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • RabbitMQ 4.1:Queues (新标签页)↗RabbitMQ · 英文

    Message Ordering:单 channel 入队顺序、多消费者、优先级和重新投递的边界。

    已阅读该小节原文;本文没有把初次投递的 FIFO 推广为业务完成顺序。

  • Apache Kafka 4.0:Introduction (新标签页)↗Apache Kafka · 英文

    主题、分区与事件键介绍:顺序保证以某个 topic-partition 的记录为范围。

    已阅读分区正文;应用并行完成和业务依赖是本文另外分析的层次。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。