FinTech
03. 金融级微服务工程实践
本章把“能调用起来”的微服务推进到“能承受金融业务”的工程实践。金融系统的核心不是技术名词,而是资金、身份、风险、合规、审计和恢复能力。
1. 金融微服务的质量目标
普通业务接口常见目标是“功能正确、性能可接受”。金融微服务还要补齐:
- 资金正确:金额、币种、方向、状态不可错。
- 可追溯:每次状态变化可查来源、操作人、请求 ID、上下游调用。
- 可恢复:失败后能重试、补偿、对账、人工处理。
- 可审计:日志、流水、审批记录、配置变更可留痕。
- 可隔离:单个依赖故障不能无限扩散。
- 可降级:非关键能力失败时核心链路仍有明确策略。
- 可演练:故障处理不能只写在文档里。
2. 领域建模:先画业务事实
以支付为例,最低限度要区分:
- 支付订单:商户发起的一次支付请求。
- 支付流水:每一次通道尝试。
- 账户流水:账户余额变化记录。
- 账务分录:借贷记账事实。
- 风控决策:对交易的风险判断。
- 通知记录:对用户、商户、内部系统发出的通知。
- 对账记录:内部账和外部通道账的核对结果。
错误示例:一个 payment 表里塞所有字段,状态随便改,失败时不知道哪一步成功。
改进示例:
- 支付订单只表达商户视角的支付状态。
- 支付流水表达通道调用状态。
- 账务服务独立记录借贷分录。
- 通知服务独立处理可重试通知。
- 对账服务后置发现差错。
3. 状态机:金融业务不要靠 if-else 漫游
支付订单示例状态:
CREATEDRISK_CHECKINGRISK_REJECTEDPAYINGPAY_SUCCESSPAY_FAILEDCLOSEDREFUNDINGREFUNDED
状态转换要回答:
- 哪个角色/系统可以触发。
- 前置状态是什么。
- 是否幂等。
- 是否产生流水。
- 是否发消息。
- 失败如何处理。
示例:
CREATED -> RISK_CHECKING -> PAYING -> PAY_SUCCESS
CREATED -> RISK_CHECKING -> RISK_REJECTED
PAYING -> PAY_FAILED
PAY_SUCCESS -> REFUNDING -> REFUNDED
数据库层可以保存:
- 当前状态。
- 状态版本号。
- 最近变更时间。
- 状态变更流水。
更新时使用乐观锁:
update payment_order
set status = 'PAY_SUCCESS',
version = version + 1,
updated_at = now()
where order_no = ?
and status = 'PAYING'
and version = ?;
如果更新行数为 0,要判断是重复通知、非法状态还是并发冲突。
4. 幂等:重试世界里的生命线
金融系统中失败经常是不确定的:
- 客户端超时,但服务端已经处理成功。
- 服务端调用通道超时,但通道后来成功。
- MQ 消息重复投递。
- 用户重复点击。
- 网关重试。
幂等设计层次:
4.1 请求幂等
同一商户订单只能创建一次支付:
create unique index uk_payment_merchant_order
on payment_order(merchant_id, merchant_order_no);
服务逻辑:
- 收到请求。
- 按唯一键插入。
- 如果唯一键冲突,查询已有订单。
- 如果参数一致,返回已有结果。
- 如果参数不一致,返回幂等冲突。
4.2 消息幂等
消费 MQ 消息时记录消费表:
create table consumed_message (
message_id varchar(64) primary key,
topic varchar(128) not null,
consumed_at timestamp not null
);
处理逻辑:
- 本地事务中先插入
message_id。 - 插入成功则执行业务。
- 主键冲突说明已处理,直接 ack。
4.3 回调幂等
通道回调可能重复。要用通道流水号、支付流水号、状态机前置状态处理,不能每次回调都重复记账。
5. 一致性:不要一上来就追求全局强事务
5.1 本地事务
单服务单数据库内,优先使用本地事务。它简单、可靠、容易排查。
5.2 Outbox/事务消息
本地数据变更和事件写入同一个数据库事务:
begin;
insert into payment_order(...);
insert into outbox_event(event_id, aggregate_id, type, payload, status);
commit;
后台任务或 CDC 将 outbox 事件发送到 MQ。好处:
- 本地状态和事件不会一成一败。
- MQ 短暂不可用不影响本地事务提交。
- 可重放、可审计。
5.3 Saga/补偿
适用于长事务:
- 创建支付。
- 冻结账户资金。
- 调用通道扣款。
- 记账。
- 通知。
如果第 3 步失败,需要解冻资金;如果第 4 步失败,可能需要补记账或进入人工处理。
补偿不是简单反向 SQL。补偿也要符合业务规则和审计要求。
5.4 TCC
Try/Confirm/Cancel 适合明确资源预留的场景,例如冻结资金:
- Try:检查并冻结。
- Confirm:扣减冻结金额。
- Cancel:释放冻结。
TCC 难点:
- 空回滚。
- 悬挂。
- 幂等。
- 超时恢复。
初学阶段先理解模式,不建议在不了解业务约束时强行套 TCC。
6. 对账:承认分布式系统会出错
对账不是“出了事故才做”,而是金融系统常规能力。
对账对象:
- 内部支付订单 vs 支付流水。
- 支付流水 vs 外部通道账单。
- 支付成功记录 vs 账务分录。
- 退款记录 vs 原支付记录。
对账流程:
- 拉取或接收外部账单。
- 标准化格式。
- 按业务主键匹配。
- 比较金额、币种、状态、时间。
- 生成差错单。
- 自动补偿或人工处理。
- 留痕。
差错类型:
- 我方成功,对方失败。
- 我方失败,对方成功。
- 金额不一致。
- 重复扣款。
- 状态延迟。
- 缺单。
7. 可观测性:技术指标和业务指标都要有
7.1 技术指标
- QPS。
- P50/P95/P99 延迟。
- 错误率。
- 超时率。
- 线程池队列长度。
- 数据库连接池使用率。
- MQ 堆积。
- GC 暂停。
7.2 业务指标
- 支付创建数。
- 支付成功率。
- 风控拒绝率。
- 通道成功率。
- 对账差错率。
- 退款成功率。
- 人工审核积压。
7.3 Trace 标签
调用链应包含:
traceIdrequestIdmerchantIdorderNopaymentIdtenantIdchannel
注意日志脱敏,不要把身份证、银行卡、手机号、完整 token 写入日志。
8. 稳定性设计
8.1 超时预算
每条链路都要有总预算。不能每层都配 3 秒,最终叠成 30 秒。
示例:
用户请求总预算:2000ms
- 网关认证:100ms
- payment 本地校验:100ms
- risk RPC:400ms
- account RPC:300ms
- 本地落库:200ms
- 预留网络和序列化:300ms
- 响应返回:100ms
- 缓冲:500ms
8.2 舱壁隔离
不要让一个下游拖垮所有请求。可以按依赖分线程池、连接池、限流器。
8.3 降级策略
降级必须业务确认:
- 营销券不可用:可不发券,但支付继续。
- 短信不可用:站内信补充。
- 风控不可用:高风险交易拒绝或人工审核。
- 账务不可用:支付核心链路是否必须阻断要严肃评审。
8.4 灰度和回滚
灰度前检查:
- 新老接口兼容。
- 数据库变更向前兼容。
- 消息消费者兼容。
- 配置中心可回滚。
- 指标和告警已接入。
回滚不是重新部署旧包这么简单。如果新版本写了旧版本不认识的数据,就可能无法回滚。
9. 发布流水线
建议流水线:
- 编译。
- 单元测试。
- 静态扫描。
- 依赖漏洞扫描。
- 构建镜像。
- 镜像扫描。
- 集成测试。
- 契约测试。
- 部署到测试环境。
- 自动冒烟。
- 预发环境压测或回归。
- 灰度发布。
- 指标观察。
- 全量发布。
- 发布后对账和巡检。
DORA 的研究长期强调软件交付能力与组织、平台和流程有关。2025 DORA 报告关于 AI 的核心提醒是:AI 会放大已有能力和问题。因此微服务团队不能只追求“生成更多代码”,而要把测试、平台、价值流、可观测性和反馈循环补齐。
10. 本章练习
练习 1:设计支付幂等表
设计表:
payment_orderpayment_request_logpayment_status_history
要求:
- 支持同一商户订单幂等。
- 支持参数不一致时识别冲突。
- 支持查询每次请求。
练习 2:设计 Outbox
当支付成功时,写出:
- 支付订单状态更新。
- Outbox 事件写入。
- 事件投递任务。
- 投递失败重试。
- 消费端幂等。
练习 3:设计对账差错处理
给出三种差错:我方成功对方失败、我方失败对方成功、金额不一致。分别写处理策略和人工介入条件。
11. 本章检查清单
- 能说清支付订单、流水、账务分录、对账记录的区别。
- 能为写接口设计幂等键和唯一约束。
- 能解释 Outbox、Saga、TCC 的适用边界。
- 能列出技术指标和业务指标。
- 能设计灰度发布前的兼容性检查。