随着心理健康 SaaS 平台架构的日益庞大,很多团队都会将原本极其臃肿的“单体应用(Monolith)”拆分成多个微服务(Microservices)。
比如,拆分出了独立的“订单支付中心”、“测评业务中心”和“消息通知中心”。
当一个 C 端用户在系统中花费 99 元购买并完成了一套极其专业的抑郁症筛查量表后,系统需要执行三个动作:
- 测评中心:在本地数据库保存他的测评报告。
- 订单中心:调用远程接口,扣减用户的余额或更新订单状态为已使用。
- 消息中心:调用远程接口,给用户发一条短信:“您的报告已生成”。
如果这三个动作是在同一个单体应用和同一个 MySQL 数据库中,只需包在一个大事务(@Transactional)里,任何一步报错,全部回滚,极其安全。
但在微服务架构下,它们分布在三台不同的服务器和三个不同的数据库里。如果在“更新订单”时因为网络抖动失败了,而前一步“保存报告”却已经成功,这就会导致极度糟糕的“数据不一致”——用户做完了测试,但订单却没有被核销。
为了解决这个难题,本文将探讨在 B/C 端心理平台中,如何极其优雅地落地分布式事务的折中方案。
为什么拒绝强一致性(2PC / XA)?
很多教科书会推荐使用两阶段提交(2PC)或 XA 协议来实现强一致性。
在这个机制下,有一个极其威严的“事务协调者”,它会强制要求三个数据库一起锁定资源,等大家都说“准备好了”,再一起提交。
这在金融银行系统里或许是必须的。但在心理测评系统中,如果你引入这种极度沉重的协调机制,系统的并发性能会呈现断崖式的暴跌,数据库连接池会被大量的锁无限期霸占。当并发流量稍微一高,整个系统就会瞬间卡死。
在互联网架构中,我们必须妥协。我们要的是高可用,而不是每一秒钟都必须保持绝对一致。
极客的妥协:最终一致性与本地消息表
正如橙星云微服务架构在处理跨服务调用时所采用的核心哲学:“BASE 理论与最终一致性”。
我们允许系统在极短的一段时间内出现数据不一致,只要在后台通过机制保证它“最终”一定会一致即可。
这里最经典的落地实践,就是本地消息表(Local Message Table)方案结合重试机制。
1. 将远程调用降级为本地写入
在测评中心保存报告的那个服务中,我们极其巧妙地在它自己的数据库里建了一张叫 message_outbox 的表。
当生成测评报告时,我们不再去同步调用订单中心的 API,而是在同一个本地数据库事务中,执行两步极其迅速的 SQL:
- 第一步:
INSERT INTO reports (插入报告记录) - 第二步:
INSERT INTO message_outbox (插入一条指令:需要去扣减订单 1024 的状态,状态为未发送)
因为这两个 INSERT 都在同一个 MySQL 实例里,利用传统的单机强事务,要么一起成功,要么一起失败。绝对不可能出现报告存了,但发件箱里没指令的情况。
2. 异步的极速搬运工
主业务流程就此结束,极其迅速地给用户返回“报告已生成”,无需等待漫长的网络调用。
而在后台,隐藏着一个极其勤勉的守护线程(或者 Cron 定时任务):
它每隔 1 秒钟去扫描 message_outbox 表,把那些“未发送”的指令取出来,通过 Kafka 或者直接调用订单中心和消息中心的 API。
如果调用成功,就把本地发件箱里的记录标记为“已发送”。
3. 六亲不认的重试机制与幂等性
如果在异步调用订单中心时,网络突然断了怎么办?
没关系,本地消息表里的记录依然是“未发送”状态。守护线程会在 1 分钟后、5 分钟后发起疯狂的重试(指数退避),直到订单中心复活并处理成功。
最核心的铁律:幂等性(Idempotency)
因为有疯狂的重试机制,订单中心可能会收到两次要求核销“订单 1024”的请求。订单中心在处理之前,必须通过唯一键(如订单号)或者 Redis 分布式锁,进行极其严格的去重判断。确保“哪怕收到 100 次同样的扣款指令,也只会扣 1 次”。
总结
在微服务的汪洋大海中,追求跨库的绝对强一致性是一个极其危险的性能陷阱。
通过引入“本地消息表”与幂等重试机制,技术团队在极度复杂的心理平台分布式架构中,用几秒钟的“暂不一致”,换取了极高并发下的系统吞吐量。这正是现代 SaaS 架构中极其精妙的妥协与极客思维。
