Appearance
当一次订单创建不再是一个事务:Service、Manager 与 Channel 的职责划分
1. 背景:传统三层结构遇到复杂业务
在很多 Java 后端项目中,最常见的结构是:
Controller
↓
Service
↓
Mapper这种结构对于简单 CRUD 业务通常已经足够。
Controller 负责接口入口,Service 负责业务逻辑,Mapper 负责数据访问。
但是随着业务开始接入支付、第三方平台、外部服务等外部渠道后,传统三层结构会逐渐暴露问题:
- 本地数据库和外部渠道不是同一个事务;
- 外部调用可能超时、失败或返回未知结果;
- 同一个业务动作可能被重复执行;
- 订单状态可能来自多个来源;
- Service 容易同时承担流程编排、数据库操作和外部调用。
本文讨论订单类业务接入外部渠道后的职责划分问题。
对于简单 CRUD 场景,Controller / Service / Mapper 已经足够;本文关注的是引入外部渠道后,本地事务、外部调用和状态确认之间如何划分职责。
2. 一个订单创建流程中的隐藏问题
一个订单创建流程通常包含:
创建订单
保存本地数据
调用外部渠道
更新订单状态从业务角度看,这是一个完整动作。
但是从系统角度看,它实际上跨越两个不同边界:
- 本地业务边界;
- 外部渠道边界。
本地数据库可以使用事务保证一致性,但外部渠道无法参与本地事务。
因此:
一个业务流程,不一定等于一个数据库事务。
例如:
本地订单保存成功
↓
调用外部渠道
↓
请求超时此时可能出现:
- 本地订单已经存在;
- 外部渠道是否成功未知。
真正的问题不是简单的"接口失败",而是:
系统如何在不确定状态下继续推进业务。
3. 从三层结构到职责边界
一个更清晰的结构:
Controller
↓
Service
├── Manager
│ ↓
│ Mapper
│
└── Channel Component
↓
External Channel| 组件 | 职责 |
|---|---|
| Service | 业务流程编排 |
| Manager | 本地业务操作单元 |
| Mapper | 数据访问 |
| Channel | 外部渠道协议边界 |
4. Service:负责业务流程编排
Service 关注:
这件业务应该按照什么顺序发生。
例如订单创建:
- 创建本地订单;
- 调用外部渠道;
- 根据结果推进状态;
- 异常时进入确认流程。
Service 不应该关心:
- 某张表如何保存;
- 某个渠道如何签名;
- 某个接口参数如何转换。
它负责连接不同边界。
5. Manager:表达本地业务操作单元
Manager 解决的问题是:
在自己的系统内部,一个业务动作如何完整完成。
例如创建订单可能涉及:
order
order_action
timeline这些数据属于同一个业务动作。
Manager 不是 Mapper 的简单包装,而是一个本地事务边界。
示意:
@Transactional
public Order createOrder(CreateOrderCommand command) {
Order order = saveOrder(command);
saveAction(order);
saveTimeline(order);
return order;
}如果只是简单查询:
Service → Mapper即可。
只有涉及多个数据对象、统一事务或完整业务动作时,才需要 Manager。
6. Channel:负责外部渠道协议边界
Channel 负责:
- 请求和响应格式;
- HTTP 通信;
- 签名;
- 验签;
- 加密;
- 错误码转换;
- 外部协议变化。
例如:
channel-wechatpay它解决:
如何正确地和外部渠道通信。
Manager 解决:
如何在自己的系统中完成一次完整业务操作。
二者不应该混合。
7. 外部调用后的状态确认
外部调用可能出现超时。
系统不能简单认为失败。
需要根据业务唯一号确认外部事实:
- 外部订单是否存在;
- 当前状态是什么;
- 是否需要更新本地状态。
创建接口负责发起业务,查询接口负责确认事实。
8. 幂等、状态约束与最终一致性
回到前面提到的重复执行问题:外部调用失败后的重试,以及异步通知重复到达,都要求系统具备幂等能力。
业务唯一号用于:
- 防止重复创建;
- 支持重试;
- 支持状态查询。
幂等不仅适用于请求重试,也适用于重复通知。
状态更新也不能简单覆盖。
系统需要限制状态流转:
- 当前状态是否允许进入目标状态;
- 是否存在状态倒退;
- 是否需要版本号或乐观锁避免并发覆盖。
本地数据库和外部渠道通常无法强一致,需要通过查询、通知、补偿等方式实现最终一致。
9. 主动查询与异步通知
外部渠道确认结果通常有两种方式:
主动查询
系统根据业务唯一号主动查询外部状态。
异步通知
外部渠道通过回调通知结果。
回调处理需要:
- 验证来源真实性;
- 验签;
- 校验业务数据;
- 遵循幂等原则。
这些属于 Channel 边界的一部分。
实际系统中,主动查询和异步通知通常需要同时存在。异步通知可以提高实时性,但由于网络延迟、通知丢失等原因,主动查询通常作为最终确认手段。
10. 数据对比能力
除了单笔查询,系统还需要具备数据对比能力。
例如:
- 获取外部渠道订单;
- 获取本地订单;
- 按业务唯一号比较。
发现:
- 外部存在,本地不存在;
- 本地存在,外部不存在;
- 双方状态不一致。
在支付、清算等领域,这类能力通常进一步发展为更完整的对账体系。
11. 什么情况下需要 Manager
不是所有调用都需要经过 Manager。
简单场景:
Controller
↓
Service
↓
Mapper复杂场景:
Controller
↓
Service
↓
Manager
↓
Mapper判断标准:
是否存在一个值得独立表达的本地业务操作单元。
需要 Manager:
- 创建订单;
- 保存多个业务对象;
- 更新多个状态。
不需要 Manager:
- 单表查询;
- 简单字段更新。
12. 总结:让不同问题归属于不同边界
最终结构:
Controller
↓
Service
├── Manager
│ ↓
│ Mapper
│
└── Channel Component
↓
External Channel| 组件 | 职责 |
|---|---|
| Controller | 接口入口 |
| Service | 业务流程编排 |
| Manager | 本地业务操作单元 |
| Mapper | 数据访问 |
| Channel | 外部渠道协议 |
本文关注的是职责划分本身。
更进一步的分布式事务方案,例如本地消息表、事务消息、Saga、TCC,以及完整对账系统设计,属于后续演进方向。