事务,是操做数据库中的数据的逻辑单元。在处理一个业务过程当中,事务保证多个数据修改的操做,要么都修改为功,要么都失败。同时,几个事务之间又相互独立,不会相互影响。html
在这篇文章中,咱们会先带你们理解事务,以及Spring中的事务,经过Spring的事务抽象引出JTA事务,以及JTA的分布式事务。理解了事务之后,再介绍分布式系统、以及分布式系统的原则,和分布式系统中实现事务的原则。java
在介绍分布式事务以前,先来来回顾一下事务的ACID原则:git
那么,在分布式系统中,这个原则是否可以保证呢?答案是不能,Not even close! 以原子性为例,在有多个系统的分布式系统中,一个分布式事务是在不一样的系统内部执行的,咱们没有办法保证它们可以同时完成,或者都不作。至于分布式事务的原则,咱们过一会再说,咱们先把事务搞清楚。github
Spring是一个伟大的框架,从一开始只是一个容器框架,到如今已经发展成为了一个包含企业开发中的方方面面的不少框架的总称。它不但从复杂度上,发展出了用于各个方面的子框架。它还从易用性出发,推出了像Spring-Boot这样的框架,使得搭建环境变得异常的简单。web
很早以前Spring就已经有了一套本身的事务规范。(在org.springframework.transaction包中),并且用起来也很是的简单:redis
1spring 2数据库 3编程 4缓存 5 6 7 8 9 10 |
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就已经有了一套本身的事务规范(在org.springframework.transaction包中)。可是针对JTA规范,Spring也作了不少工做,使得咱们在实现事务的时候不须要关心具体用的是哪个。可是,这也有一些问题:
第一个问题就是,不少作Java开发的人,都不知道JTA和Spring Transaction的区别。
第二,这两种规范在某些地方仍是有些区别,使用不当也会出现问题。
咱们在上面介绍本地事务的时候说,使用Spring框架的标签@Transactional,来方便的实现事务,可是须要说明的是,这个标签类,在Spring的事务以及JavaEE事务规范中都有定义,分别是:
咱们在使用的时候就须要注意,若是你用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)的资源,如:
EntityManager
中得到EntityTransaction
,经过它实现。session
:jms的session
提供commit()和rollback()操做。若是查看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 |
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以前,先看看XA。
XA是由X/Open组织提出的分布式事务的架构(或者叫协议)。XA架构主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间造成通讯桥梁。也就是说,在基于XA的一个事务中,咱们能够针对多个资源进行事务管理,例如一个系统访问多个数据库,或即访问数据库、又访问像消息中间件这样的资源。这样咱们就可以实如今多个数据库和消息中间件直接实现所有提交、或所有取消的事务。XA规范不是java的规范,而是一种通用的规范,
目前各类数据库、以及不少消息中间件都支持XA规范。
JTA是知足XA规范的、用于Java开发的规范。因此,当咱们说,使用JTA实现分布式事务的时候,其实就是说,使用JTA规范,实现系统内多个数据库、消息中间件等资源的事务。
JTA(Java Transaction API),是J2EE的编程接口规范,它是XA协议的JAVA实现。它主要定义了:
javax.transaction.TransactionManager
,定义了有关事务的开始、提交、撤回等操做。javax.transaction.xa.XAResource
,一种资源若是要支持JTA事务,就须要让它的资源实现该XAResource
接口,并实现该接口定义的两阶段提交相关的接口。若是咱们有一个应用,它使用JTA接口实现事务,应用在运行的时候,就须要一个实现JTA的容器,通常状况下,这是一个J2EE容器,像JBoss,Websphere等应用服务器。可是,也有一些独立的框架实现了JTA,例如Atomikos, bitronix都提供了jar包方式的JTA实现框架。这样咱们就可以在Tomcat或者Jetty之类的服务器上运行使用JTA实现事务的应用系统。
在上面的本地事务和外部事务的区别中说到,JTA事务是外部事务,能够用来实现对多个资源的事务性。它正是经过每一个资源实现的XAResource
来进行两阶段提交的控制。感兴趣的同窗能够看看这个接口的方法,除了commit, rollback等方法之外,还有end()
, forget()
, isSameRM()
, prepare()
等等。光从这些接口就可以想象JTA在实现两阶段事务的复杂性。
在上面介绍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的缘由。
若是你的分布式系统只是把数据库按照功能地区等进行分区分片的划分,再使用MQ等资源,那你就彻底能够经过使用JTA来实现不一样资源的分布式事务。
可是,如今流行的微服务框架,每每是部署多个服务,一个事务可能须要调用多个服务,调用多个数据库、MQ,对于这种微服务架构的分布式事务,又须要使用其它的方式来实现。
分布式系统从一开始到如今,有多种形式,从应用的个数和使用的数据库角度来讲,简单列举了以下几种:
这几年,微服务的概念愈来愈火,通常来讲,用微服务架构实现的分布式系统,有两种方式(这里简化了不少东西,像缓存、监控、消息中间件、日志等等支持系统,只是仅仅考虑通常的业务系统):
按照功能划分,每一个应用提供某种功能,而每一个应用又部署多个实例来实现高可用。
这种实现的好处是能够每一个功能一个应用,那个每一个应用模块都比较简单;可是,这就会有不少服务间调用,有时候为了完成一个业务请求,要在好几个服务之间调用好几回。这就须要在拆分模块、设计服务间调用的接口、设计业务的流程的时候都须要综合考虑,尽可能减小服务间调用。
还有一种是用一样的应用部署多个实例,各个服务经过分区分片等方式,使用各自的数据库或其它数据源。
这种方式的好处就是,全部的功能都在一个应用里,一个应用部署任意多个,只须要经过合理的数据库的分区分片,让不一样的节点访问不一样的数据库。可是,一旦你的数据愈来愈复杂,数据库的分区分片会很是复杂。有时候,也能够把数据库按功能分开,一个节点访问多个数据库。
在介绍如何实现分布式系统的事务以前,咱们先看看分布式系统的原则。
对于分布式系统来讲,很难有一个相似ACID这样的标准,和知足这个标准的开发规范,及其实现的框架,来为咱们方便的实现分布式系统的事务。要实现分布式系统的事务,咱们每每须要根据实际须要,在可用性(包括性能、系统吞吐量等)、事务性(相似本地事务的ACID)、可操做性(开发和维护的难易程度)之间作出权衡。
那么,分布式事务的原则是什么呢?咱们怎么能肯定一个分布式事务的实现,知足了它的事务性的要求呢?首先,咱们来看看分布式系统的一个原则,或者叫定理,CAP定理。
CAP定理,包括如下几个方面:
为了便于咱们理解,在介绍这个CAP定理的时候,咱们结合一个业务实例来看看这个CAP定理是什么意思。这个实例是一个简单的订票系统的购票流程,大体以下:
这是一个微服务架构的分布式系统,有一个网关统一接受用户请求,而后将请求转发到相应的服务上。
总共有3个服务:Order,User,Ticket分别用于处理交易、用户、票相关的业务。
每一个服务都使用本身的数据库。
一个购票流程大体以下:
再来看看CAP定理:
因为分布式系统形式的多样性和复杂性,若是想彻底知足上述的原则设计一个分布式系统,几乎是不可能的。首先,分布式服系统就是要把系统的各个部分部署到不一样的服务器上,那咱们就必需要经过分区容错来避免因为网络、机器故障等缘由形成的问题。因此分区容错性是必不可少的,不然可用性都没法保证。
对于可用性来讲,若是咱们要严格保证可用性,即便是在分区容错性获得保障的前提下,全部的服务都是可用的,有时候,咱们也须要经过异步的方式来处理一些业务,这就会形成数据的不一致。如已经从用户帐户上扣费,可是票尚未转移完成等。
再来看一致性,是否有办法可以实现呢?那咱们就须要先来看看几种一致性:
在通常的分布式系统的设计中,咱们大都以最终一致性为目标,来设计咱们的分布式事务。这既能保证系统的可用性和容错性,也能在绝大多数状况下保证数据的弱一致性,而且在少数出错或网络高延迟的状况下,也能保证数据的最终一致性。
上面说了分布式事务的原则,以最终一致性为目标,那为了实现这个目标,我就须要不少异常状况的处理,包括数据库失败、业务代码失败、网络错误等。举例来讲,在一个接口调用,若是发生超时,我就须要重试。可是也有可能对方的服务已经处理完这个请求,只是在完成返回结果的时候,网络传输的问题致使超时。那么服务调用端再重试,实际上就是发了两次请求。因此,我就须要对于分布式服务的事务处理,对于一样的消息,只会处理一次。
分布式系统的幂等性,就是对于一个处理接口,若是它会对系统形成反作用,也就是修改数据,那就须要保证对于一样的请求,无论请求了多少次,结果都是一致的。
那么,如何保证这个幂等性呢?一种比较通用的方法就是,对每个请求,生成一个token
,并且须要惟一,而后将这个token放在请求的参数里面。服务在处理这个请求以前,先拿到token,检查这个token是否已经处理过,只有没有处理过的才去处理。这个token能够保存在数据库、redis、甚至内存等地方。因为它只是用来记录已经处理的请求的token,因此大可没必要保存在内存中。因为在分布式系统中,一个服务会部署多个,一个请求失败后从新发送,有多是被发送到另外一台机器上。因此这个token应该是服务范围共享的,咱们须要在同一个服务的多个部署都能共享访问的地方,来保存已经处理过的token。
因此,使用redis来保存token是一个不错的选择。
分布式事务的实现,也有不少种的实现方式,通常称做模式。咱们在一个分布式系统的一个业务方法里,每每须要调用外部的数据库、MQ,也有可能调用其它服务。若是你把其它的服务也看做一种资源,那么一个业务方法实际上就是操做了好几个资源。而这,正是XA所作的事情。可是,不是全部的资源都支持XA,像咱们的服务间调用,通常就只有一个处理方法,不会提供什么commit()
、rollback()
之类的方法,更别说两阶段提交须要的其它方式。
可是,咱们却能用事务的思想为咱们实现分布式事务提供一些启发。从本地事务的处理过程,咱们能够看出,它是经过:1.尝试修改 2.提交(完成) 3.取消(出错)的方式来实现的 。根据这个思想,咱们能够想到分布式事务的几种实现方式:
有关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事物,还有跨多个资源的事物,也介绍了一下分布式系统和分布式事务,特别是分布式事务的强一致性原则。在以后的几篇文章中,将继续介绍实现分布式事务的几种具体的方法。