⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7302308334203486245 「躺_平」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

我们在开发的过程中,如果一个业务操作需要本地写MYSQL数据以及对第三方系统做写操作,那么这种流程就涉及到分布式系统一致性的问题,然而并非所有系统都能使用成熟的分布式事务方案

PS:示例代码推送到:

https://gitee.com/dailycreatebug/demo-codes

案例说明

以一个财务报账业务为例,涉及到的系统如下:

系统名 作用 实现方案
单据系统 申请单内容以及凭证的生成 JAVA
BPM 实现流程的运转 购买成熟系统(例如:泛微)
SAP 财务凭证 购买成熟系统

详细解释下各系统作用:

  • 单据系统: 财务报账,会提交很多信息(例如:报账事由、报账金额与明细)。同时也会生成财务凭证(不了解凭证也没关系,它就是给财务人员看的东西,对技术人员来说就是数据库的一堆数据)
  • BPM系统: 非常成熟的流程管理系统,以非常直观的方式来实现流程的搭配,不了解的可以自行百度扫盲。在此案例中,需要使用BPM的两个能力:1)调用API,审核通过 2)调用API,获取流程的待审人
  • SAP系统: 财务专用系统,不用过多了解,只要知道在财务审核完成后,会将单据系统生成的凭证数据通过API调用的方式发送给SAP即可

“审核通过”业务流程

当审核人员审核通过时,大致流程如下:

  • 保存业务数据+记录审核日志
  • 调用BPM接口,审核通过
  • 调用BPM接口,获取最新待审人
  • 如果没有待审人,说明已经审完,生成凭证并推送SAP

代码如下

风险分析

如图所示,如果在1和2出现异常,由于有事务的存在,操作1内的几条mysql写操作会被回滚,因此所有数据都没有任何变化。

但如果1和2正常执行,操作3发生异常,操作1的数据会因为事务回滚,但操作2并不能。因此整个系统会出现一个很诡异的现象:单据系统内,没有任何日志记录,用户操作的数据也没有保留下来,但BPM那边却已经审核通过了,这在任何正常流程中,都是不可能出现的状态。

对于用户而言,他在页面会收到报错,然后可能会再次点击“审核通过”,而此时BPM那边却显示,流程已经走到下一个节点,该用户无权限操作。

问题分析

根本原因其实不难,因为MYSQL事务只能管他自己,没法控制第三方系统

解决思路

一个字:拆!

对于分布式系统,没有任何人能保证远程调用不出问题,因此在做设计时,就必须能够对这种情况做出应对

上面的操作,打包放进一个大事务就是根因,因此方案就是将大事务拆开,在拆分时,需要遵循以下几个原则:

  • 小事务内,尽量只有一个远程写操作
  • 该远程写操作放到方法最后,保证在其返回成功后就能立刻提交事务
  • 小事务可能会因为某些原因失败,因此需要机制来进行重试

整体思路就是这样

基于以上原则,改动如下

第一步,新建一张任务表:

CREATE TABLE `transaction_job` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`type` varchar(255) NOT NULL COMMENT '任务类型',
`data` varchar(255) NOT NULL COMMENT '任务数据',
`error_message` varchar(255) DEFAULT NULL COMMENT '错误信息',
`context` varchar(255) DEFAULT NULL COMMENT '任务上下文(主要是保存当前操作人)',
`create_time` bigint(20) NOT NULL COMMENT '创建时间',
`update_time` bigint(20) NOT NULL COMMENT '更新时间',
`retry_times` int(11) NOT NULL DEFAULT '0' COMMENT '重试次数',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='事务任务表';

作用:保存小事务的关键数据到data字段中,以保证通过该字段,就能正确执行小事务。另外也需要保存当前操作人的信息context。

第二步,通过定时任务,查出transaction_job表中未完成的数据,并执行对应的操作,这里通过简单的策略模式,将框架代码和业务代码做了分离

首先是框架代码核心逻辑

扫描任务

执行任务

这是一个策略模式接口,每个小事务就封装一个独立的实现类

其中更新待审人的任务实现如下,其实就是把那部分代码复制过来

第三步,改造业务代码,不再一次性把流程写完,而且是在第一个小事务中,顺便往 transaction_job中插入一条数据,以执行第二个小事务

其中步骤2(插入任务数据的代码)的具体实现如下(transactionJobService.create)

优化

上述方案种,除第一个事务外,后续事务都是通过定时任务来执行的,因此这些事务都存在一定的延迟,用户体验不好,解决办法也非常简单,只需要利用好Spring对于事务的生命周期管理,稍微改造一下插入任务的方法(transactionJobService.create)

这是Spring提供的工具类,afterCommit()方法会在事务提交后执行,因此加上这段代码后,思路就变成了这样

如果小事务顺利执行,会立刻将该任务改为“成功”,因此从用户端是感受不到延迟的

注意

可能存在添加任务后,定时任务也立刻扫描到了这条数据,同一任务就会被主线程与定时任务线程同时执行,所以实际应用中需要考虑这个问题(比如:加锁再执行,执行前再检查数据库状态)

结语

目前只给出了解决思路的核心,但真实项目中,还添加了不少额外功能,以后会逐渐更新进来

文章目录
  1. 1. 前言
  2. 2. 案例说明
    1. 2.0.0.1. “审核通过”业务流程
    2. 2.0.0.2. 风险分析
    3. 2.0.0.3. 问题分析
  • 3. 解决思路
  • 4. 优化
    1. 4.0.0.1. 注意
  • 5. 结语