华为没有公开 MetaERP 的“消息中间件白皮书”,但结合其公开架构(云原生微服务、GaussDB、事件驱动、SLA/子账—总账模型)以及大型 ERP 的通用做法,可以把 MetaERP 在分布式事务里“事件不丢、不重、不乱序”的机制讲得很具体。
核心结论先给:
MetaERP 不是靠“消息队列本身保证一切”,而是用
本地事务落库 + 事务消息/发件箱 + 分区有序 + 消费幂等 + 对账补偿
把“可靠投递”和“顺序性”分开解决:
可靠靠持久化与确认,顺序靠业务键分区,正确靠幂等和重放。
1. 可靠投递:先保证“事件一定产生且不会丢”
1.1 本地消息表 / Outbox Pattern
业务服务和事件存储在同一本地事务里:
BEGIN TX INSERT invoice_header / invoice_lines INSERT ar_open_item INSERT outbox_event( event_id, aggregate_id, -- 发票ID event_type, -- AR_INVOICE_CREATED payload, status='PENDING', seq_no, version ) COMMIT要么业务单据和事件一起成功,要么一起失败。
不会出现在数据库里发票有了、事件没发;或事件发了、发票回滚。
这是 MetaERP 替代 Oracle EBS“同库触发器”的关键:
- EBS:一个库里 PL/SQL 写完 AP/AR/GL
- MetaERP:AR 服务自己落库,事件出域后再驱动会计引擎/GL
1.2 事务消息 / 发件箱转发
Outbox 里的事件由可靠组件投递到消息总线(RocketMQ / Kafka 类组件):
- 发送前:status = PENDING
- MQ 返回 ACK:status = SENT
- 消费方确认处理完:可再标记 PROCESSED(可选)
如果发送失败:
- 定时扫描器重捞 PENDING / SENT-BUT-NOT-CONSUMED
- 按 event_id 重发
- MQ 本身持久化、副本同步、消费者拉取,避免 broker 丢消息
1.3 At-Least-Once,而不是 Exactly-Once
ERP 领域通常接受:
事件可能重复,但绝不能丢。
所以 MetaERP 的设计目标是:
- 投递语义:At-Least-Once
- 业务语义:Effectively-Once(通过幂等实现)
2. 顺序性:不是“全局有序”,而是“按业务对象有序”
这是最关键的一点。
应收模块不会要求全系统所有事件全局保序,那样吞吐会崩。
它要的是:
同一张发票 / 同一个客户 / 同一笔合同 的事件,不能乱序。
2.1 用 Aggregate Key 做分区键
事件带业务主键:
场景 | 分区键 |
|---|---|
发票生命周期 | invoice_id |
客户回款核销 | customer_account_id |
合同收入确认 | contract_id |
预收款 | prepayment_id |
消息队列里:
- 同一 partition key → 进同一 partition
- partition 内 FIFO
- 消费者单线程处理该 partition
于是:
AR_INVOICE_CREATED AR_INVOICE_APPROVED AR_INVOICE_POSTED AR_RECEIPT_APPLIED AR_INVOICE_CLOSED不会被处理成:
RECEIPT_APPLIED → INVOICE_NOT_YET_CREATED2.2 事件带版本号 / 序列号
每条事件有:
aggregate_id event_version = 1,2,3... event_time global_log_offset(MQ offset)消费者处理前检查:
if incoming.version != current_version + 1: reject / reorder / park如果出现:
- 旧事件迟到 → 丢弃或进异常处理
- 缺版本 → 暂停该聚合、触发补事件 / 重放
3. 消费端:用幂等把“重复”和“乱序”消掉
3.1 事件 ID 去重表
会计引擎 / GL 接收侧:
processed_event ( event_id PK, source_system, journal_batch_id, status )再来相同 event_id:
- 直接返回成功
- 不再生成第二笔凭证
所以即使 MQ 重发 3 次,GL 也只有 1 笔应收凭证。
3.2 业务状态机校验
应收对象有状态:
DRAFT → VALIDATED → POSTED → PARTIALLY_RECEIPTED → CLOSED收到事件时:
- POSTED 事件:当前必须是 VALIDATED
- RECEIPT_APPLIED:发票必须已 POSTED
- CLOSED:未清金额必须为 0
非法转换:
- 进异常事件表
- 告警
- 人工或自动补偿,而不是“硬算”
3.3 SLA / 会计引擎的幂等映射
同一业务事件:
- 主账簿:借应收 / 贷收入
- IFRS 账:按履约义务
- 管理账:加利润中心维度
但都来自同一个event_id,不会“中国账记了、IFRS 账重复记”。
4. 跨服务一致性:Saga + 补偿,而不是 2PC
MetaERP 不会用传统两阶段提交把 AR、会计引擎、GL、现金、税务锁在一起(那样月结会死)。
而是:
AR 本地事务:发票成立 → 发 AR_INVOICE_POSTED 会计引擎:生成 SLA 日记账 → 发 ACCOUNTING_CREATED GL:汇总过账 → 发 GL_JE_TRANSFERRED 现金:回款事件 → 发 RECEIPT_CREATED 核销服务:应用收款某一步失败:
- 不回滚上游数据库
- 发补偿事件 / 标记异常
- 例如会计引擎挂了:
- 发票已在 AR 成立
- 会计事件滞留
- 重放后补凭证
- 月结前“未生成会计事件报表”会拦住关账
这就是最终一致性 + 关账强校验。
5. 顺序被破坏时怎么办?
MetaERP 通常有四道防线:
① 分区有序:正常路径不出错
按 invoice_id / customer_id 分区。
② 版本号拦截:迟到事件不污染状态
旧事件进来直接 reject。
③ 异常事件队列
- 乱序
- 状态不合法
- 金额对不上
- 原单不存在
进入:
ar_event_exception由规则或人工处理。
④ 月结对账兜底
关账前跑:
- AR 未清项总额 = 发票 - 收款 - 贷项
- SLA 日记账 = AR 业务事件数
- GL 应收统驭 = 子账汇总
- 多账簿来源事件一致
只要“事件丢了 / 重复了 / 顺序错了导致少记”,关账报表就会爆红。
6. 用应收场景串一遍
以“发票 → 收款 → 核销”为例:
- AR 服务提交发票
→ 同事务写outbox: AR_INVOICE_POSTED - 事件转发到 MQ
→ topic:ar.events,key:invoice_123 - 会计引擎消费
→ 检查event_id是否已处理
→ 生成 SLA 凭证
→ 发ACCOUNTING_POSTED - GL 消费
→ 按source_event_id去重
→ 应收统驭科目 +1000 - 回款服务发
RECEIPT_CREATED
→ key:customer_55 - 核销服务消费
→ 先见发票 POSTED,再见回款
→ 更新未清项 - 若回款事件比发票先到:
→ 发票不存在 / 状态不对
→ 进异常队列
→ 等发票事件补处理后重放
7. 一句话总结
MetaERP 保证事件可靠性和顺序性的真实模型是:
本地事务落 Outbox 保“不丢”
按聚合键分区保“业务内有序”
版本号 + 状态机保“乱序可发现”
event_id 幂等保“重复无害”
Saga 补偿 + 月结对账保“最终正确”
它不是“消息队列顺序有多强”,而是:
即使消息重复、乱序、延迟,财务结果依然可对、可追、可重放、可关账。