本文以一个实际业务问题来谈谈事务该如何处理。对接外部系统是是不可避免的,从广泛意义上来说,外部系统范围很大,中间件(数据库)也属于外部系统。当我们讨论事务时,通常我们将那些没有支持事务的系统称为外部系统,业务系统基本上都是外部系统。

问题

有这样一套系统,以gitlab为底层系统, 在gitlab project的基础上封装了代码仓,系统对其中一些与gitlab关联的数据进行了落表。创建代码仓的逻辑过程比较复杂,首先通过gitlab创建代码仓(其中返回的代码仓id需要落库),系统表code_repo插入记录,创建gitlabhook,系统表member插入记录(维护系统用户和对应代码仓的关系),创建多个gitlabbranch(系统业务,初始化代码仓可以初始化多个分支),系统表label插入记录。

分析

以上大概说明整个业务流程, gitlab接入的方式采用HTTP,不采用直接对接gitlab数据库的原因是,其创建项目和分支业务逻辑复杂,梳理代价很大。对接任何系统其实本质上可以概括为两种:

对接方式 优势 缺点
上游系统提供的接口 不需要关注上游系统的自身义务逻辑,维护只需要关注接口版本 灵活性较差
上游系统底层存储(数据库,文件等) 需要梳理和维护上游系统业务逻辑,维护是代码层面的 灵活性、可控性很好

,数据库采用mysql。 在不考虑异常的情况,整个业务流程还是比较清晰的,但是分布式的核心就是处理各种异常情况,这也是分布式复杂的地方。因为分布式的网络环境是很复杂的。我们很保证底层系统的可用性。在这里,当gitlab其中的接口出现异常,系统仍落库显然是不合理的,同样的当数据库发生异常,gitlab数据存在也是不合理的,分布式事务的核心要解决数据一致性。

解决还需要与当前的项目架构不能冲突。目前各服务还没有拆分,运行。数据库也没有拆分。其架构仍是单体架构模式,设计时考虑了后期的微服务拆分。目前主要问题是解决GitlabDB之间的数据一致性,其复杂度没有微服务多DB的高。

分布式事务(浅谈)

事务的概念来自数据库事务,在数据库事务定义中,事务是一个执行的逻辑单元,它需要提供一个一致、可靠的数据操作,它主要包括下面两个目标:

  1. 当出现任何错误,包括系统宕机、部分失败,都能保证左右的数据修改都能够恢复到未修改的状态;
  2. 不通的事务并发处理相同的数据时,提供适当的隔离机制。 我们常说的ACID,其实是某些数据库特有的事务实现方式,也就是实现了原子性、一致性、隔离性和持久性。 dan 分布式事务,一直是实现分布式系统过程中最大的挑战,在单个数据源的系统中,只要该数据源支持事务,例如大部分关系型数据库,一些MQ服务(redis,mysql,activeMQ等),我们实现事务相对容易。那么我们如何在分布式系统中实现事务呢?这就要从分布式系统的原则说起,目前主要有BASE原理 ACP原理。其中ACP是:
  3. A:可用性(Availability)
  4. B:一致性(Consistency)
  5. C:分区容错性(Tolerance of network Partition) ruan zhuang t爱 A和P没什么好说的,就是分布式系统的基本特性,C(一致性)指在分布式系统中,多个节点之间的数据的一致性,包括一个节点修改的数据,通过另一个节点访问的时候也能够看到;以及当一个操作需要修改多个数据源的数据,多个修改都能够同时完成或者同时不完成。

这里的一致性,我们可以看作是数据库事务的ACID特性中,原子性、一致性甚至是隔离性的统一。如果以ACID标准实现分布式系统,在现实中是不可能的。其中原子性就没法实现,如果一个业务请求,要修改多个数据库中的数据,那么这多个数据库操作,就无法实现原子性,势必会有一个先后,这一点点时间就违反了原子性。

所以,我们往往无法在分布式系统中实现完全的一致性,所以就有了BASE理论,BASE是BasicallAvailable(基本可用),SOftstate(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,要求实现最终一致性即可。

其中,SoftState(软状态)是指,在一个业务操作过程中,允许出现一个中间状态,也就是软状态,而不是要求原子性那样,要么完成要么不完成。例如在下单的时候,出现一个正在处理状态。由于有这个软状态,那我的一致性就不是要求抢一致性,而是最终一致性,也就是说,只要最终这个请求能够处理完,所有数据状态都是处理完的状态;如果期间出错了,所有的数据也都一致,该失败的失败,该退钱的退钱,该重置的重置。 确定分布式系统的实现原则是最终一致性以后,同时也明确了我们实现分布式事务的原则,也是最终一致性。其实,不管数据库事务的ACID特性,还是分布式事务的最终一致性,都是根据事务的定义和它的两个目标,所采用的不通的实现方式。

那我们该如何实现这个最终一致性呢? y俄都是

单服务的分布式事务

首先,任何一个分布式系统,总是由一个个的系统组成,也就是一个个的服务,这些服务也可以部署多个实例。同时,我们整个系统也需要一定的方式相互通信。我们采用接口的方式,也可以通过MQ之类的消息中间件通信。但是无论怎样,分布式系统会涉及多个数据源。

对于这种每个服务访问多个数据源的情况,其实就是一个最简单的分布式事务的场景。如果大家在网上搜“Spring分布式事务实现”,搜索到结果也就是在说这个场景下的分布式事务实现过程。要实现这个事务,首先需要对Spring的事务机制有一定的了解。对于这种情况,最简单的就是使用Springde JTA事务管理,但是我们知道。JTAs事务管理是通过两阶段提交实现的,在很多情况下,他的效率是很低的。因为它在多个数据源秀修改数据的时候,这些数据一直处在被锁的状态,直到多个数据源的事务都提交完成,才会释放。

如果不用JTA,Spring也给我们提供了几种方式,来近似的实现分布式事务,例如:

  1. 事务同步,也就是提交一个事务的时候,通过Listener等方式通知另一个事务也提交。但是这种情况下,如果第二个事务提交出错了,第一个事务就无法回滚了,因为它已经提交完成了
  2. 链式事务,也就是将多个事务,包装在一个链式事务管理器中,当提交事务的时候,一次提交里面的事务,对于这种情况,也存在上面所说的问题

所以,使用spring在单服务多数据源的情况下,实现分布式事务,实际上没办法完全实现事务的,因为出错的时候不能够保证。那么这时候,就需要通过其他机制来补充。比如重试,自己处理错误和异常。
大家可以试想一下,分布式系统越复杂,它出错的情况就越多,我们需要考虑的补救措施就越多。这种修修补补的实现分布式事务的最终一致性的做法,始终不是一个好做法。但是,使用Spring解决单服务的分布式系统,始终是分布式事务实现的基础。我们可以用其他的模式来方便我们解决分布式事务,但是在每个服务中,我们还是要经常使用事务同步、链式事务等来实现事务。我们用Spring来保证绝大数情况下的事务问题,而对于特殊的错误情况,就采用其他的模式来解决。

分布式事务实现的模式

刚才说了我们使用其他模式来处理分布式事务问题,主要有下面五种模式,参考 分布式事务参考

  1. XA方案
  2. TCC方案
  3. 本地消息表
  4. 可靠消息最终一直方案
  5. 最大努力通知方案

解决

由于目前是单DB,可以尽最大限度利用单DB事务特性,Spring事务管理中介绍了有两种事务管理方式(programmatically and declarative )。 虽然官方文档建议使用申明式事务管理方式,但是我们在回滚数据库事务需要执行其他操作时,如API的反操作,手动方式才能够实现。

1
2
3
4
5
6
7
8
public void resolvePosition() {
try {
// some business logic...
} catch (NoProductInStockException ex) {
// trigger rollback programmatically
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}

CoderepoService中提供的创建代码仓接口createCodeRepo,主要包含①②③④操作,其中只有一个DB操作④,使用数据库事务无法满足情况。 例如当①②执行成功,③执行失败,这是通过异常事务回滚无法回滚Gitlab中的数据。所以必须手动回滚,处理逻辑如下。我们Spring数据库事务,保证DB操作, 当③④失败时手动捕获异常(回滚gitlab数据),并抛出系统业务异常(使事务生效)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Transaction
public ServiceTSingleResponse<CodeRepoDTO> add(CodeRepoSaveRequest request) throw Exception e {
①创建gitlab项目;
②DB插入数据;
try{
③创建gitlab hook;
④创建多个分支
}catch(Exception e){
deleteGitlabProject(projectId);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new BaseException();
}
...
}

这里涉及的Gitlab操作对应一个回滚操作deleteGitlabProject(),DB通过事务回滚。

总结

本文讨论问题类似单服务多DB场景,不同的是gitlab数据与通是保证通过HTTP调用的,并没有实现事务。 如何保证DB与第三方系统数据保持一致性的问题,尽量较小的代码改动。在微服务多数据源的情况下不能够满足需求,需要根据业务选型。