REST微服务的分布式事务实现-分布式系统、事务以及JTA介绍

事务,是操做数据库中的数据的逻辑单元。在处理一个业务过程当中,事务保证多个数据修改的操做,要么都修改为功,要么都失败。同时,几个事务之间又相互独立,不会相互影响。html

在这篇文章中,咱们会先带你们理解事务,以及Spring中的事务,经过Spring的事务抽象引出JTA事务,以及JTA的分布式事务。理解了事务之后,再介绍分布式系统、以及分布式系统的原则,和分布式系统中实现事务的原则。java

事务的ACID原则

在介绍分布式事务以前,先来来回顾一下事务的ACID原则:git

  • 原子性(A):原子性是指一个事务的全部操做,要么都作完,要么都不作。
  • 一致性(C):一致性是指一个事务的执行,无论外部环境如何,无论怎么执行,结果应该都是一致的。
  • 隔离性(I):隔离性是指几个事务在同时执行的时候,相互之间不会受影响。
  • 持久性(D):持久性就是事务完成之后,数据就被保存。

那么,在分布式系统中,这个原则是否可以保证呢?答案是不能,Not even close! 以原子性为例,在有多个系统的分布式系统中,一个分布式事务是在不一样的系统内部执行的,咱们没有办法保证它们可以同时完成,或者都不作。至于分布式事务的原则,咱们过一会再说,咱们先把事务搞清楚。github

Spring中使用事务

Spring是一个伟大的框架,从一开始只是一个容器框架,到如今已经发展成为了一个包含企业开发中的方方面面的不少框架的总称。它不但从复杂度上,发展出了用于各个方面的子框架。它还从易用性出发,推出了像Spring-Boot这样的框架,使得搭建环境变得异常的简单。web

很早以前Spring就已经有了一套本身的事务规范。(在org.springframework.transaction包中),并且用起来也很是的简单:redis

 

1spring

2数据库

3编程

4缓存

5

6

7

8

9

10

 

@Service

public Class OrderService {

@Transactional

public TicketOrder buyTicket(OrderDTO orderDTO) {

TicketOrder tkOrder = new TicketOrder();

jdbcTemplate.execute(createOrderSQL);

return tkOrder;

}

}

 

咱们只须要在方法上加一个Transactional标签,那个这个方法就会在一个事务里面执行。这是用代理模式实现的。Spring容器在初始化这个service实例的时候,其实是建立一个代理类,而后在调用这个方法的时候,包装一个事务的处理。上面的方式使用代理模式展开,大体以下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

 

public Class OrderServiceProxy {

// 经过代理实现的伪代码,在原先的代码外再包一层事务的建立、commit、rollback

public Order buyTicket(OrderDTO orderDTO) {

// get transaction from entityManager

theTransaction.begin();

try {

orderServiceImpl.buyTicket(orderDTO)

theTransaction.commit();

} catch(Exception e) {

theTransaction.rollback();

throw e;

}

}

}

从这个流程能够看出,在更改数据的时候jdbcTemplate.save(order),事务并无提交,用户查看最新的数据的时候,也看不到这条数据(隔离性),只有commit之后,全部的数据修改才会同时起效(原子性)。若是期间发生任何错误,事务就会回退rollback,全部的数据修改又回到未修改状态。

可能不少Java开发人员对事务的了解,就到这一步,没有再往下了解过。咱们如今就来了解一下Spring的事务管理,了解了Spring的事务,才能知道如何在分布式系统中使用Spring的事务管理实现分布式事务。

Spring的事务抽象

因为历史缘由,从很早以前,Spring就已经有了一套本身的事务规范(在org.springframework.transaction包中)。可是针对JTA规范,Spring也作了不少工做,使得咱们在实现事务的时候不须要关心具体用的是哪个。可是,这也有一些问题:
第一个问题就是,不少作Java开发的人,都不知道JTA和Spring Transaction的区别。
第二,这两种规范在某些地方仍是有些区别,使用不当也会出现问题。

咱们在上面介绍本地事务的时候说,使用Spring框架的标签@Transactional,来方便的实现事务,可是须要说明的是,这个标签类,在Spring的事务以及JavaEE事务规范中都有定义,分别是:

  • org.springframework.transaction.annotation.Transactional
  • javax.transaction.Transactional

咱们在使用的时候就须要注意,若是你用Spring boot,那么大部分状况下这两种标签都能使用。spring boot提供了不少自动配置,可以根据你是否包含了JTA的依赖,来判断是否要使用JTA的事务。若是没有,即便你用的javax.transaction.Transactional标签,也会使用spring的事务机制来处理。

可是,若是你的spring应用比较复杂,有不少自定义的配置,就须要注意这两种标签。最好是根据须要明确的使用一种。

Spring之因此可以实现对两种事务的支持,是由于在spring的Transaction规范中,定义了一个统一的PlatformTransactionManager事务管理器。即便你没有使用某个JPA框架,而是直接用JDBTemplate,Spring也可以使用默认的DataSourceTransactionManager来使用JDBC的事务来实现事务。并且也能够直接经过标签org.springframework.transaction.annotation.Transactional来实现事务。也就是说,你不须要任何JPA的实现框架,只是使用Spring-Transaction就能实现数据库的事务。

Spring的PlatformTransactionManager,也有JTA的实现JtaTransactionManager。也就是说,你可使用Spring的事务规范,却使用JTA的实现,并且也几乎不须要任何配置,只要在具体的运行环境中包含包含JTA的实现能够。好比你用JBoss的应用服务器,系统就会使用Jboss的JTA实现;若是你的class path里面有Atomikos的库,系统就会使用Atomikos的JTA实现。若是你使用spring-boot,你只须要在你的依赖里面、或运行环境里面,提供你所须要的JTA实现,它就会自动使用。

除了数据库,spring的事务还支持JMS的事务,也就是在经过JMS使用某个消息中间件时,也能用spring的事务来实现读写消息的事务。

再总结一下Spring的事务抽象,它定义了抽象的事务管理,能够管理任何支持事务操做(也就是commit和rollback)的资源,如:

  1. JDBC Connection: JDBC的链接支持commit()和rollback()操做。。
  2. JPA的Entity: 从JPA的EntityManager中得到EntityTransaction,经过它实现。
  3. JMS的session:jms的session提供commit()和rollback()操做。
  4. JTA的事务:JTA的事务固然提供了事务的操做。

本地事务和全局事务

若是查看Spring事务相关的文档,常常会看到’local transactions’和’external transactions’,本地事务和全局事务(或叫外部事务)。上面咱们说了,对任何资源,只要它提供了事务的操做,咱们就能使用spring的事务管理来提供事务。因为spring提供了一个事务管理的抽象接口,而事务的控制,能够是在spring容器来控制,也能够由外部的事务管理模块来控制,这就是本地事务和全局事务的区别。

本地事务就是指的是由Spring容器建立和维护的事务。例如在使用JDBC事务操做数据库的时候,spring容器会在须要的时候建立事务的上下文,开启一个JDBC的事务,而后调用业务方法,执行完成后,调用commit方法;而后在出错的时候调用资源的rollback方法。还有事务的传播、隔离等也都是由Spring容器来提供。本地事务只能针对一个资源实现彻底的事务控制。若是要在一个本地事务中操做两个资源(例如两个数据库),实际上前后在两个数据库的Connection上调用commit()方法去提交。

而外部事务,就是spring只负责经过事务的接口来开始事务、提交事务、回滚事务,而具体的操做仍是得有外部提供的事务管理的模块或组件来执行和维护。例如咱们使用JBoss来运行咱们的web应用,而后在JBoss上配置了JTA的事务。那么事务的具体管理和维护就是由JBoss提供的事务管理模块来进行。

本地事务和外部事务的一个主要区别就是,是否能对多个资源实现事务控制。咱们来经过一个例子来讲明它们的区别:使用JDBCTemplate和JMSTemplate对一个数据库和一个MQ进行操做。使用Spring的代码大体以下:

 

1

2

3

4

5

6

7

8

9

10

 

@Service

public Class OrderService {

@Transactional

public TicketOrder buyTicket(OrderDTO orderDTO) {

orderRepository.save(order);

jmsTemplate.convertAndSend("order:need_to_pay", order);

return tkOrder;

}

}

也就是在@Transactional标记的方法里,经过jdbcTemplate操做数据库,使用JMSTemplate操做MQ。这个方法虽然是在一个事务里,可是,若是咱们使用本地事务,那么这两个资源(数据库和MQ)其实是在各自的事务里面分别操做。把这段代码展开成它实际的样子,大体以下:

 

1

2

3

4

5

6

7

8

9

10

11

 

jmsTransaction.begin(); // get transactions from jms session

dbTransaction.begin(); // get transactions from JDBC connection

try {

orderRepository.save(order);

jmsTemplate.convertAndSend("order:need_to_pay", dto);

dbTransaction.commit();

jmsTransaction.commit();

} catch(Exception e) {

dbTransaction.rollback();

jmsTransaction.rollback();

}

这样,若是上述代码在jmsTransaction.commit();的时候出错,这时候数据库的事务已经提交,就没法回滚。若是这时候这个方法被从新执行,数据库的操做就会被重复执行。

若是咱们使用外部事务,那么这里就不会针对两个资源出现两个事务,而是只有一个事务,来统一管理多个资源。若是在多个资源上的事务出错了,外部的事务也可以保证回滚,这是经过事务的两阶段提交(2PC)来实现。使用JTA实现的事务正是这种外部事务。

因为JTA使用两阶段提交来实现多个资源之间的事务,这就会带来很大的性能问题。由于它要同步多个资源的事务,对每一个资源使用两阶段提交,这就使得这个事务所花的时间比本地事务多不少。并且在这个时间段内,因为事务的隔离性,可能会形成长时间的资源占用,使得其它的事务没法同步访问该资源上的一些数据。

JTA事务

在上面已经屡次提到JTA事务,那么JTA究竟是什么呢?介绍JTA以前,先看看XA。

XA

XA是由X/Open组织提出的分布式事务的架构(或者叫协议)。XA架构主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间造成通讯桥梁。也就是说,在基于XA的一个事务中,咱们能够针对多个资源进行事务管理,例如一个系统访问多个数据库,或即访问数据库、又访问像消息中间件这样的资源。这样咱们就可以实如今多个数据库和消息中间件直接实现所有提交、或所有取消的事务。XA规范不是java的规范,而是一种通用的规范,

目前各类数据库、以及不少消息中间件都支持XA规范。
JTA是知足XA规范的、用于Java开发的规范。因此,当咱们说,使用JTA实现分布式事务的时候,其实就是说,使用JTA规范,实现系统内多个数据库、消息中间件等资源的事务。

什么是JTA

JTA(Java Transaction API),是J2EE的编程接口规范,它是XA协议的JAVA实现。它主要定义了:

  • 一个事务管理器的接口javax.transaction.TransactionManager,定义了有关事务的开始、提交、撤回等操做。
  • 一个知足XA规范的资源定义接口javax.transaction.xa.XAResource,一种资源若是要支持JTA事务,就须要让它的资源实现该XAResource接口,并实现该接口定义的两阶段提交相关的接口。

若是咱们有一个应用,它使用JTA接口实现事务,应用在运行的时候,就须要一个实现JTA的容器,通常状况下,这是一个J2EE容器,像JBoss,Websphere等应用服务器。可是,也有一些独立的框架实现了JTA,例如Atomikosbitronix都提供了jar包方式的JTA实现框架。这样咱们就可以在Tomcat或者Jetty之类的服务器上运行使用JTA实现事务的应用系统。

在上面的本地事务和外部事务的区别中说到,JTA事务是外部事务,能够用来实现对多个资源的事务性。它正是经过每一个资源实现的XAResource来进行两阶段提交的控制。感兴趣的同窗能够看看这个接口的方法,除了commit, rollback等方法之外,还有end()forget()isSameRM()prepare()等等。光从这些接口就可以想象JTA在实现两阶段事务的复杂性。

JPA

在上面介绍Spring的事务抽象的时候,说过Spring事务支持JPA的事务,这里再针对这个作一下说明。
可能不少作Java开发的,都没弄清楚JTA和JPA的区别,即便我一开始也觉得JTA和JPA就是一回事,其实否则。JPA是Java Persistence API,也就是Java持久化编程接口。它定义了Java对象和它的持久化之间的联系,也就是Java的Object和Relation之间的Mapping,也就是一般说的ORM。而这个对象的持久化,不只限于数据库,也多是NoSQL,多是文件,也多是其它可以序列化后保存的地方。JPA使用@Entity标记一个Java对象,并将这个Java对象和数据库的某一个表关联。经过ID将一个实例映射到表中的一条记录。
Hibernate就是一个实现JPA的框架。
使用JPA实现事务,用的是EntityManager来获取一个事务EntityTransaction,而在JTA中,用的是TransactionManager

JTA分布式事务

使用JTA事务,能够实现对多个资源实现事务,这也是一说到分布式事务,就会说JTA的缘由。

若是你的分布式系统只是把数据库按照功能地区等进行分区分片的划分,再使用MQ等资源,那你就彻底能够经过使用JTA来实现不一样资源的分布式事务。

可是,如今流行的微服务框架,每每是部署多个服务,一个事务可能须要调用多个服务,调用多个数据库、MQ,对于这种微服务架构的分布式事务,又须要使用其它的方式来实现。

分布式系统

分布式系统从一开始到如今,有多种形式,从应用的个数和使用的数据库角度来讲,简单列举了以下几种:

  1. 一个服务同时访问多个数据库,有多个数据源。
  2. 对一个应用部署多个实例,每一个实例都无差异的为用户提供服务,同一个应用的多个实例可能访问同一个数据库。
  3. 同一个应用部署多个实例,各个实例经过对数据库的分片分区,来访问不一样的数据库。
  4. 按照功能划分,不一样的功能由不一样的服务提供,相互以前经过网关、服务中心等方式通讯。

这几年,微服务的概念愈来愈火,通常来讲,用微服务架构实现的分布式系统,有两种方式(这里简化了不少东西,像缓存、监控、消息中间件、日志等等支持系统,只是仅仅考虑通常的业务系统):
按照功能划分,每一个应用提供某种功能,而每一个应用又部署多个实例来实现高可用。
多服务多实例

多服务多实例

这种实现的好处是能够每一个功能一个应用,那个每一个应用模块都比较简单;可是,这就会有不少服务间调用,有时候为了完成一个业务请求,要在好几个服务之间调用好几回。这就须要在拆分模块、设计服务间调用的接口、设计业务的流程的时候都须要综合考虑,尽可能减小服务间调用。

还有一种是用一样的应用部署多个实例,各个服务经过分区分片等方式,使用各自的数据库或其它数据源。
单一服务多实例

单一服务多实例

这种方式的好处就是,全部的功能都在一个应用里,一个应用部署任意多个,只须要经过合理的数据库的分区分片,让不一样的节点访问不一样的数据库。可是,一旦你的数据愈来愈复杂,数据库的分区分片会很是复杂。有时候,也能够把数据库按功能分开,一个节点访问多个数据库。

分布式系统的原则

在介绍如何实现分布式系统的事务以前,咱们先看看分布式系统的原则。

对于分布式系统来讲,很难有一个相似ACID这样的标准,和知足这个标准的开发规范,及其实现的框架,来为咱们方便的实现分布式系统的事务。要实现分布式系统的事务,咱们每每须要根据实际须要,在可用性(包括性能、系统吞吐量等)、事务性(相似本地事务的ACID)、可操做性(开发和维护的难易程度)之间作出权衡。
那么,分布式事务的原则是什么呢?咱们怎么能肯定一个分布式事务的实现,知足了它的事务性的要求呢?首先,咱们来看看分布式系统的一个原则,或者叫定理,CAP定理。

CAP定理

CAP定理,包括如下几个方面:

  • 一致性(C):在分布式系统中的全部数据备份,在同一时刻是否一样的值。好比说,在购票流程处理的过程当中,若是用户看到本身的余额以及被扣了,那么它应该也能看到票夹里的票、以及支付完成的订单。
  • 可用性(A):可用性是指系统提供的服务必须一直处于可用的状态,包括每一个请求都应该在必定的时间内返回结果。它包括时间和结果两个条件,也就是说,即便出现错误、超时等问题,也应该是必定的时间内给用户反馈。
  • 分区容错性(P):若是集群系统中有一部分服务发生故障,仍然可以保证对外提供知足一致性和可用性的服务。也就是说,集群中一部分节点故障后,集群总体仍是能响应客户端的读写请求。

为了便于咱们理解,在介绍这个CAP定理的时候,咱们结合一个业务实例来看看这个CAP定理是什么意思。这个实例是一个简单的订票系统的购票流程,大体以下:
ticket-process.png

这是一个微服务架构的分布式系统,有一个网关统一接受用户请求,而后将请求转发到相应的服务上。
总共有3个服务:Order,User,Ticket分别用于处理交易、用户、票相关的业务。
每一个服务都使用本身的数据库。

一个购票流程大体以下:

  1. 用户触发一个购票流程
  2. Order服务处理这个请求,先在当前服务建立订单等信息,将数据保存在order数据库
  3. 而后调用User服务,进行扣费等操做
  4. 再调用Ticket服务,进行票的转移等操做
  5. 整个流程完成后,用户应该能看到本身的订单,本身余额的减小,以及票夹里的票。

再来看看CAP定理:

  • 一致性(C):一致性就是说,在购票流程处理的过程当中,若是用户看到本身的余额以及被扣了,那么它应该也能看到票夹里的票、以及支付完成的订单。
  • 可用性(A):可用性就是在购票过程当中,要及时给用户反馈,即便有些步骤时间比较长,能够经过异步的方式处理,给用户一个正在处理的结果。
  • 分区容错性(P):假设某一个Ticket服务的节点出了故障,也应该有其它的节点可以提供Ticket服务;若是全部的Ticket服务都不能用了,也不该该影响用户访问其它的部分,而只是不能下单买票。

因为分布式系统形式的多样性和复杂性,若是想彻底知足上述的原则设计一个分布式系统,几乎是不可能的。首先,分布式服系统就是要把系统的各个部分部署到不一样的服务器上,那咱们就必需要经过分区容错来避免因为网络、机器故障等缘由形成的问题。因此分区容错性是必不可少的,不然可用性都没法保证。
对于可用性来讲,若是咱们要严格保证可用性,即便是在分区容错性获得保障的前提下,全部的服务都是可用的,有时候,咱们也须要经过异步的方式来处理一些业务,这就会形成数据的不一致。如已经从用户帐户上扣费,可是票尚未转移完成等。
再来看一致性,是否有办法可以实现呢?那咱们就须要先来看看几种一致性:

  • 强一致:当更新操做完成以后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这就像本地事务的原子性(A)和隔离性(I)的统一。在分布式系统中,若是一个业务处理须要多个系统都更新数据,那就要求多个系统的更新同时完成。可是,由于它们的不一样的系统,‘同时完成‘须要服务间的协做、同步才能完成,在完成以前,用户不能看到更新后的数据,也不能看到更新前的(由于要强一致),因此用户只能等待。这就违背了可用性;同时,为了保证强一致性,须要作不少额外的工做,又大大增长了出错的可能性。因此在分布式系统中,强一致性通常都没法实现。
  • 弱一致性:系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功以后,不承诺当即能够读到最新写入的值,也不会具体的承诺多久以后能够读到。例如上面的例子,咱们有几个子系统,当订单系统生成订单,而后交由用户系统处理的时候,这时候用户就可以看到本身的新的订单。当票务系统处理票的转移的时候,用户能看到已经扣费,可是又看不到票夹里的票。虽然这个时间可能很短,可是也是存在的。
  • 最终一致性:弱一致性的特定形式。系统保证在没有后续更新的前提下,系统最终返回上一次更新操做的值。例如上面的例子中, 咱们用异步方式处理用户的购票,先生成订单、扣费,异步处理票,返回给用户结果,这时用户看到的订单状态是正在处理,只有整个流程处理完了,用户才能看到订单结束,而且能看到买到的票。除了异步操做形成的一致性问题之外,还有在某一个节点发生故障的状况下,经过重试、取消等机制,或者人工参与,使得系统的数据也能最终达到一个一致的状态。

在通常的分布式系统的设计中,咱们大都以最终一致性为目标,来设计咱们的分布式事务。这既能保证系统的可用性和容错性,也能在绝大多数状况下保证数据的弱一致性,而且在少数出错或网络高延迟的状况下,也能保证数据的最终一致性。

幂等性原则

上面说了分布式事务的原则,以最终一致性为目标,那为了实现这个目标,我就须要不少异常状况的处理,包括数据库失败、业务代码失败、网络错误等。举例来讲,在一个接口调用,若是发生超时,我就须要重试。可是也有可能对方的服务已经处理完这个请求,只是在完成返回结果的时候,网络传输的问题致使超时。那么服务调用端再重试,实际上就是发了两次请求。因此,我就须要对于分布式服务的事务处理,对于一样的消息,只会处理一次。

分布式系统的幂等性,就是对于一个处理接口,若是它会对系统形成反作用,也就是修改数据,那就须要保证对于一样的请求,无论请求了多少次,结果都是一致的。

那么,如何保证这个幂等性呢?一种比较通用的方法就是,对每个请求,生成一个token,并且须要惟一,而后将这个token放在请求的参数里面。服务在处理这个请求以前,先拿到token,检查这个token是否已经处理过,只有没有处理过的才去处理。这个token能够保存在数据库、redis、甚至内存等地方。因为它只是用来记录已经处理的请求的token,因此大可没必要保存在内存中。因为在分布式系统中,一个服务会部署多个,一个请求失败后从新发送,有多是被发送到另外一台机器上。因此这个token应该是服务范围共享的,咱们须要在同一个服务的多个部署都能共享访问的地方,来保存已经处理过的token。

因此,使用redis来保存token是一个不错的选择。

分布式事务的实现

分布式事务的实现,也有不少种的实现方式,通常称做模式。咱们在一个分布式系统的一个业务方法里,每每须要调用外部的数据库、MQ,也有可能调用其它服务。若是你把其它的服务也看做一种资源,那么一个业务方法实际上就是操做了好几个资源。而这,正是XA所作的事情。可是,不是全部的资源都支持XA,像咱们的服务间调用,通常就只有一个处理方法,不会提供什么commit()rollback()之类的方法,更别说两阶段提交须要的其它方式。

可是,咱们却能用事务的思想为咱们实现分布式事务提供一些启发。从本地事务的处理过程,咱们能够看出,它是经过:1.尝试修改 2.提交(完成) 3.取消(出错)的方式来实现的 。根据这个思想,咱们能够想到分布式事务的几种实现方式:

  • 用先尝试修改、再确认或取消的方式,也就是TCC模式(Try尝试修改 - Commit提交 - Cancel取消)
  • 还有一种稍微简单的就是Fallback模式,就是先修改,出错了之后再进行相应的处理,也就是调用Fallback。
  • 除了这两种之外,还有一种经常使用的就是使用消息中间件,各个分布式系统在处理同一个事务的时候,经过消息驱动来执行各自的任务。

有关TCC模式的详细内容,请参考做者的原文。TCC模式的事务实现,咱们会在另外一篇文章中再介绍。

有关在spring中实现分布式事务,有一篇文章。这篇文章介绍了使用XA和不使用XA实现分布式事务的几种方式。可是都是说的同时使用2种或以上的资源(如数据库和MQ等支持XA的数据源)的状况。虽然不能适用于微服务架构的服务间调用的状况,可是也能有一些借鉴意义。
在上面的文章中介绍的其中一种方式是:最大努力一阶段提交。仍是用以前的例子说明,也就是在一个事务方法中操做DB和MQ:

 

1

2

3

4

5

6

7

8

9

10

 

@Service

public Class OrderService {

@Transactional

public TicketOrder buyTicket(OrderDTO orderDTO) {

jdbcTemplate.doQuery(sth);

jmsTemplate.send(sth);

return tkOrder;

}

}

 

若是这里不用JTA事务,而是使用Spring的本地事务,那么这个方法内的操做执行完之后,spring事务管理器会前后提交DB Connection的事务和JMS Session的事务。在这种方式下,绝大部分状况下都不会有问题。可是有可能出现的一种错误就是,在提交完一个事务后,提交另外一个事务的时候出错了。在以最终一致性为目标的分布式事务中,每每就容许这种状况的出现,可是须要采用另外一些措施来补救。

至此,咱们介绍了Spring事物、JTA事物,还有跨多个资源的事物,也介绍了一下分布式系统和分布式事务,特别是分布式事务的强一致性原则。在以后的几篇文章中,将继续介绍实现分布式事务的几种具体的方法。

相关文章
相关标签/搜索