技术博客

消息中间件在交易链路的使用:可靠投递与顺序性的取舍

第 92 / 141 篇 · 2026-03-17 · secfin · 消息队列系统架构可靠性

互联网系统的经典画法是"一切异步化":订单进 MQ,库存、支付、通知各自消费。把这幅画搬进券商核心系统会直接翻车。这篇讲 MQ 在核心系统里真实的用与不用,以及两个绕不开的取舍——可靠投递与顺序性。

主链路为什么不用 MQ

一笔委托的延迟预算通常这样切:客户端到柜台 5ms,事前风控 2ms,柜台到撮合往返 3ms,回报落库 2ms,全链留给网络的余量不到 5ms。MQ 在这笔预算里没有位置:

  • 多一跳就多一个故障域。进程间调用挂了是单点失败;MQ 挂了是所有依赖方集体阻塞,还要处理网络分区下"这条消息到底发出去没有"的悬案——producer 超时重试,可能重发,下游可能重复,语义开始变浑。
  • 交易是同步应答业务。客户点买入,界面要在几百毫秒内给出"已报/废单"的明确结果。MQ 的语义是"发出去就算成功",和"必须拿到结果"是错配的。主链路要的是请求-应答,不是发布-订阅。
  • 顺序性在主链路是硬约束。同一账户先买后卖、先冻结后扣减,顺序错了就是透支。分布式队列只能保证分区有序,分区键一旦选错(比如按消息类型),同一账户的两条消息就散到不同分区了。

所以柜台、风控、撮合、回报落库这条链,全部是进程内调用或点对点直连。这不是技术保守,是延迟、故障域、业务语义三件事都不匹配。

MQ 用在哪:可重放、可异步、可最终一致的地方

真实系统里 MQ 的落点高度集中,都有同一个特征:消费方崩了消息不丢,重放就行。

第一是回报分发。一条成交确认,风控要算敞口、实时估值要刷新、报送系统要攒监管数据、CRM 要推通知。让柜台挨个同步调四个系统,任何一个慢了都拖住回报路径。做法是柜台只干一件事——把回报 append 进流水表(《核心系统的数据库设计》那套 append-only 结构),然后发一条 MQ 消息。四个订阅方各自消费,谁崩了谁的 lag 涨,修好追平即可,源头流水一行不少。

第二是批量作业的触发与通知。日终清算几百个任务,任务间的依赖通知走消息表或 MQ,配合数据库的栅栏同步做并发控制(《清算任务编排》篇展开过)。批量场景天然异步,MQ 是顺手的选择。

第三是外围集成。银证转账结果、两融额度调整、第三方资讯推送,这些本来就接受秒级甚至分钟级延迟,MQ 的解耦价值在这里发挥得最干净。

可靠语义:at-least-once 与幂等消费

先把语义说清楚,这是踩坑的重灾区。RocketMQ 和 Kafka 提供的是 at-least-once:消息至少送达一次,可能重复。Kafka 文档里的 exactly-once 指的是流处理作业内部的端到端语义,不是"消费方只收到一次"。任何把 MQ 当"不会重复"来用的设计都是 bug 种子。

消费端幂等的工程做法很固定:消息带业务唯一键(资金流水号、成交回报序号),消费方落一张消费记录表,主键就是唯一键,插入冲突即视为已消费:

-- 消费去重表:幂等的全部秘密
CREATE TABLE mq_consume_log (
    msg_key      VARCHAR(64) PRIMARY KEY,  -- 业务唯一键,如回报序号
    topic        VARCHAR(64),
    consume_time TIMESTAMP
);
-- 消费流程:INSERT 成功才执行业务,主键冲突直接 ACK 跳过

这个结构顺手解决了重启恢复:consumer 重启后没 ACK 的消息重投,重投撞上主键冲突被丢弃。代价是消费表本身要维护,量大时按天分区和定期归档。

还有一个更隐蔽的洞:消费成功但 ACK 之前进程挂了,消息重投,业务却已执行过一次。所以幂等表必须和业务写入在同一个本地事务里——"查记录→执行业务→写记录"这三步之间断电,照样重复。

顺序性:分区键的选择就是业务建模

Kafka 只能保证 partition 内有序,RocketMQ 是队列级有序。要保序就要让所有需要保序的消息进同一个分区,分区键因此是业务决策:

  • 按账户号哈希分区:同一账户的资金流水、持仓变动有序到达,清算消费端不用重排。这是金融系统的默认选择。
  • 代价是热点账户被钉死在一个分区上,吞吐上限 = 单分区吞吐。普通账户无所谓,但自营户、产品户的流水量可能是散户的一千倍,单分区会被打满。
  • 折中:普通路径按账户哈希;识别出的热点账户走单独通道(消费方直接读流水表绕过 MQ),或者热点账户独占分区。

还有一种场景干脆放弃 MQ 保序:估值、对账这类按批量重放的消费方,根本不在意到达顺序,拿到消息先攒起来,按流水序号排序后再处理。批量消费方自己排序,比指望 MQ 保序可靠——反正它手里有全量流水表兜底。

背压与积压监控

MQ 在核心系统里最危险的状态不是挂,是积压不报警。消费方慢半拍,lag 悄悄涨,三天后批量任务追不平才发现。两条硬规矩:交易时段 lag 超阈值直接告警并触发降级(比如实时风控链路降级为读流水表补偿);每个订阅方单独监控 lag——四个订阅方共用一个消费者组是常见错误,一个慢的拖累全部。

委托主链路不用 MQ,MQ 只出现在可重放的分支上 交易柜台 同步应答 毫秒预算 事前风控 + 撮合 进程内通信 不走 MQ 回报流水 append-only 丢一条可重放 客户应答 同步返回 消息队列 账户哈希分区 at-least-once 订阅方 风控·报送 CRM·估值 主链路三关:毫秒预算 / 故障域 / 同步应答语义——MQ 全挂,直连是唯一合理选择 MQ 落点三特征:可重放(流水表兜底)、可异步、可最终一致;回报分发与批量触发都满足 保序靠分区键(账户哈希),幂等靠消费表(业务唯一键),两者都是业务建模决策

一句话收束:MQ 在券商核心系统里的位置,由"这条链路允不允许异步、丢不丢得起、顺序错了有没有兜底"三个问题决定。主链路三关全挂,回报分发三关全过——选型从来不是技术题,是业务语义题。