周末的技术分享会上某位同事分享的一个案例牵扯出了分布式事务的问题,引发了一些讨论,这里记录一下。
现有的生产环境中核心业务现在分成三个项目,姑且叫 A、B、C 吧。A 负责日常营业相关的前台入口,比如用户注册登记、下订单等等;B 负责后台系统管理人员的资料维护,日常业务数据统计等;C 是衍生出来的项目,就是把原有 A、B 中涉及的共通的业务抽取出来,形成的一个 Core 项目,包括了用户、账户、商品相关业务等。因为随着业务访问量的上升,原有的业务表和逻辑会不断扩充,数据量也不断增加,再突破单机容量极限之前项目肯定会拆分来开发、部署、实施。
问题就产生在这种拆分的情况下,比如,原有的下订单的逻辑直接在 A 项目中的一个 Controller 层调用一个服务,几张业务表(比如商品扣减库存、扣减账户余额,生成相关订单)搞定了,所有事务交给 A 项目的 Spring 框架的事务管理搞定,期间调用相关业务服务的某次失败(比如恰好在下单瞬间用户余额不足或库存不够了)会使得整个事务回滚(这点 Spring 的事务管理器做的已经很出色)。
但现在将项目拆分开来,项目和项目之间是分开部署,它们之间的调用走的是 RPC ,所以原来一个下订单的服务(AOrderService.method)涉及了本身 A 项目里面的服务和 C 项目里面的服务的调用,比如这个场景涉及 AService.method1、AService.method2、CService.method3、AService.method4 。假如 AService.method1、AService.method2、CService.method3 执行成功、AService.method4 执行失败,怎么办?因为AService.method4 执行失败了,会抛出异常,而且 AService.method1、AService.method2、AService.method4 同在一个项目中,所以事务框架会自动回滚前面执行的 SQL ,可是 CService.method3 是在 C 项目中,其事务是单独维护的,这次请求调用它的执行是成功的,所以 C 项目中事务已经提交了。
针对这个问题的解决方式我想到了下面几种:
把 AService.method1、AService.method2、AService.method4 这三个服务也抽出来放到 C 项目中,使这些方法始终在一个项目中,从而规避了事务的分布式困扰。这个对业务限制太大,久而久之将形成一个薄的 Controller 层 A 项目和巨无霸臃肿的 C 项目。
如果 CService.method3 和 AService.method4 之间没有执行上的前后依赖关系的话,把CService.method3 挪到整个 AOrderService 的方法最后调用。这是种打补丁的做法,现在勉强可用,后续扩展开来没发搞,比如再增加一个 B 项目的服务调用还是会出问题。
搬出分布式事务问题的学院式解法:两阶段提交(XA),Java 针对这个规范提供了 JTA 的解决方案,这个可以倒是可以,但是太重,并且延迟和并发上难以忍受。
事务补偿。即通过实时消息来解耦不同项目间的调用,其间一旦发现项目的调用失败了会反过来通知整个业务链,使得整个业务回滚。这好像是此问题的一般做法了,但它要求所有这些业务接口满足两个条件:业务接口的调用是幂等的,业务接口都存在反向的逆操作接口。
事务补偿的变通形式,即定时任务扫业务表,发现失败自动执行脚本回滚业务,这种方案是异步并有一定延迟的,并且前提是项目足够小,不然这脚本做出来都很复杂难懂。