首先想要了解分布式事务,得先了解什么是事务。能够浏览我上面写过的事务的博客java
Spring事务原理分析-部分一mysql
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不一样的分布式系统的不一样节点之上。简单的说,就是一次大的操做由不一样的小操做组成,这些小的操做分布在不一样的服务器上,且属于不一样的应用,分布式事务须要保证这些小操做要么所有成功,要么所有失败。本质上来讲,分布式事务就是为了保证不一样数据库的数据一致性。github
当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,具体分库分表的原理在此不作解释,之后有空详细说,简单的说就是原来的一个数据库变成了多个数据库。这时候,若是一个操做既访问01库,又访问02库,并且要保证数据的一致性,那么就要用到分布式事务。web
所谓的SOA化,就是业务的服务化。好比原来单机支撑了整个电商网站,如今对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候若是要同时对订单和库存进行操做,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就须要用到分布式事务。spring
原子性是指事务包含的全部操做要么所有成功,要么所有失败回滚,所以事务的操做若是成功就必需要彻底应用到数据库,若是操做失败则不能对数据库有任何影响。sql
一致性是指事务必须使数据库从一个一致性状态变换到另外一个一致性状态,也就是说一个事务执行以前和执行以后都必须处于一致性状态。数据库
拿转帐来讲,假设用户A和用户B二者的钱加起来一共是5000,那么无论A和B之间如何转帐,转几回帐,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。编程
隔离性是当多个用户并发访问数据库时,好比操做同一张表时,数据库为每个用户开启的事务,不能被其余事务的操做所干扰,多个并发事务之间要相互隔离。api
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始以前就已经结束,要么在T1结束以后才开始,这样每一个事务都感受不到有其余事务在并发地执行。
关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即使是在数据库系统遇到故障的状况下也不会丢失提交事务的操做。
例如咱们在使用JDBC操做数据库时,在提交事务方法后,提示用户事务操做完成,当咱们程序执行完成直到看到提示后,就能够认定事务以及正确提交,即便这时候数据库出现了问题,也必需要将咱们的事务彻底执行完成,不然就会形成咱们看到提示事务处理完毕,可是数据库由于故障而没有执行事务的重大错误。
最经典的场景就是支付了,一笔支付,是对买家帐户进行扣款,同时对卖家帐户进行加钱,这些操做必须在一个事务里执行,要么所有成功,要么所有失败。而对于买家帐户属于买家中心,对应的是买家数据库,而卖家帐户属于卖家中心,对应的是卖家数据库,对不一样数据库的操做必然须要引入分布式事务。
买家在电商平台下单,每每会涉及到两个动做,一个是扣库存,第二个是更新订单状态,库存和订单通常属于不一样的数据库,须要使用分布式事务保证数据一致性。
XA是一个分布式事务协议,由Tuxedo提出。XA中大体分为两部分:事务管理器和本地资源管理器。其中本地资源管理器每每由数据库实现,好比Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器做为全局的调度者,负责各个本地资源的提交和回滚。XA实现分布式事务的原理以下:
总的来讲,XA协议比较简单,并且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。可是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,每每并发量很高,XA没法知足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换会致使主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得很是狭隘。
所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操做成功成功而且对外发消息成功,要么二者都失败,开源的RocketMQ就支持这一特性,具体原理以下:
一、A系统向消息中间件发送一条预备消息
二、消息中间件保存预备消息并返回成功
三、A执行本地事务
四、A发送提交消息给消息中间件
经过以上4步完成了一个消息事务。对于以上的4个步骤,每一个步骤均可能产生错误,下面一一分析:
步骤一出错,则整个事务失败,不会执行A的本地操做
步骤二出错,则整个事务失败,不会执行A的本地操做
步骤三出错,这时候须要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,若是失败则回滚预备消息
步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不须要,其实经过回调接口,消息中间件可以检查到A执行成功了,这时候其实不须要A发提交消息了,消息中间件能够本身对消息进行提交,从而完成整个消息事务
基于消息中间件的两阶段提交每每用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操做+发消息)+B系统的本地操做,其中B系统的操做由消息驱动,只要消息事务成功,那么A操做必定成功,消息也必定发出来了,这时候B会收到消息去执行本地操做,若是本地操做失败,消息会重投,直到B操做成功,这样就变相地实现了A与B的分布式事务。原理以下:
虽然上面的方案可以完成A和B的操做,可是A和B并非严格一致的,而是最终一致的,咱们在这里牺牲了一致性,换来了性能的大幅度提高。固然,这种玩法也是有风险的,若是B一直执行不成功,那么一致性会被破坏,具体要不要玩,仍是得看业务可以承担多少风险。
另外此方案严重依赖消息中间件的高可用性,一旦消息中间件挂了整个系统就挂了。洪峰到来时,全部的系统都依赖消息中间件的话,消息中间件很容易就挂了。
所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操做。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,若是更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是经过代码人为实现了两阶段提交,不一样的业务场景所写的代码都不同,复杂度也不同,咱们今天着重讲解TCC编程模式的代码实现。
TCC是一种补偿性分布式事务解决方案,最初由支付宝提出。TCC是三个英文单词的首字母缩写,分别对应Try、Confirm和Cancel三种操做,这三种操做的业务含义以下:
Try:预留业务资源
Confirm:确认执行业务操做
Cancel:取消执行业务操做
稍稍对照下关系型数据库事务的三种操做:DML、Commit和Rollback,会发现和TCC有殊途同归之妙。在一个跨应用的业务操做中,Try操做是先把多个应用中的业务资源预留和锁定住,为后续的确认打下基础,相似的,DML操做要锁定数据库记录行,持有数据库资源;Confirm操做是在Try操做中涉及的全部应用均成功以后进行确认,使用预留的业务资源,和Commit相似;而Cancel则是当Try操做中涉及的全部应用没有所有成功,须要将已成功的应用进行取消(即Rollback回滚)。其中Confirm和Cancel操做是一对反向业务操做。
简而言之,TCC是应用层的2PC(2 Phase Commit,两阶段提交),若是你将应用看作资源管理器的话。详细来讲,TCC每项操做须要作的事情以下:
一、Try:尝试执行业务。
完成全部业务检查(一致性)
预留必须业务资源(准隔离性)
二、Confirm:确认执行业务。
真正执行业务
不作任何业务检查
只使用Try阶段预留的业务资源
三、Cancel:取消执行业务
释放Try阶段预留的业务资源
TCC的理论有点让人费解。故接下来将以帐务拆分为例,对TCC事务的流程作一个描述,但愿对理解TCC有所帮助。帐务拆分的业务场景以下,分别位于三个不一样分库的账户A、B、C,A和B一块儿向C转账共80元:
一、Try:尝试执行业务。
完成全部业务检查(一致性):检查A、B、C的账户状态是否正常,账户A的余额是否很多于30元,账户B的余额是否很多于50元。
预留必须业务资源(准隔离性):账户A的冻结金额增长30元,账户B的冻结金额增长50元,这样就保证不会出现其余并发进程扣减了这两个账户的余额而致使在后续的真正转账操做过程当中,账户A和B的可用余额不够的状况。
二、Confirm:确认执行业务。
真正执行业务:若是Try阶段账户A、B、C状态正常,且账户A、B余额够用,则执行账户A给帐户C转帐30元、账户B给帐户C转帐50元的转账操做。
不作任何业务检查:这时已经不须要作业务检查,Try阶段已经完成了业务检查。
只使用Try阶段预留的业务资源:只须要使用Try阶段账户A和账户B冻结的金额便可。
三、Cancel:取消执行业务
释放Try阶段预留的业务资源:若是Try阶段部分红功,好比账户A的余额够用,且冻结相应金额成功,账户B的余额不够而冻结失败,则须要对账户A作Cancel操做,将账户A被冻结的金额解冻掉。
幂等性就是用户对于同一操做发起的一次请求或者屡次请求的结果是一致的,不会由于屡次点击而产生了反作用。
tcc-transaction是开源的TCC补偿性分布式事务框架
Git 地址: github.com/changmingxi…
咱们目前使用的版本是1.2,引入tcc-transaction工程。
安装tcc-transaction-api tcc-transaction-core 和 tcc-transaction-dubbo到本地仓库
(1)pom.xml中引入依赖
<dependency>
<groupId>org.mengyun</groupId>
<artifactId>tcc-transaction-dubbo-capital-api</artifactId>
<version>1.2.4.14</version>
</dependency>
<dependency>
<groupId>org.mengyun</groupId>
<artifactId>tcc-transaction-dubbo</artifactId>
<version>1.2.4.14</version>
</dependency>
<dependency>
<groupId>org.mengyun</groupId>
<artifactId>tcc-transaction-spring</artifactId>
<version>1.2.4.14</version>
</dependency>
复制代码
(2)web.xml上下文加载tcc-transaction
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:config/spring/local/appcontext-*.xml,classpath:tcc-transaction.xml,classpath:tcc-transaction-dubbo.xml
</param-value>
</context-param>
复制代码
发布一个Tcc服务方法,可被远程调用并参与到Tcc事务中,四个约束:
(1)在服务提供方的实现方法上加上@Compensable注解,并设置注解的属性
(2)在服务提供方的接口方法上加上@Compensable注解
(3)服务方法的入参能被序列化(默认使用jdk序列化机制,须要参数实现Serializable接口,能够设置repository的serializer属性自定义序列化实现)
(4)try方法、confirm方法和cancel方法入参类型须同样
Compensable的属性包括propagation、confirmMethod、cancelMethod、transactionContextEditor。propagation可不用设置,框架使用缺省值;设置confirmMethod指定CONFIRM阶段的调用方法;设置cancelMethod指定CANCEL阶段的调用方法;设置transactionContextEditor为DubboTransactionContextEditor.class。
发布Tcc服务示例:
try接口方法:
@Compensable
public String record(CapitalTradeOrderDto tradeOrderDto);
复制代码
try实现方法:
@Compensable(confirmMethod
= "confirmRecord", cancelMethod = "cancelRecord",
transactionContextEditor = DubboTransactionContextEditor.class)
public String record(CapitalTradeOrderDto tradeOrderDto) {}
复制代码
confirm方法:
public void confirmRecord(CapitalTradeOrderDto tradeOrderDto) {}
复制代码
cancel方法:
public void cancelRecord(CapitalTradeOrderDto tradeOrderDto) {}
复制代码
调用远程Tcc服务,将远程Tcc服务参与到本地Tcc事务中,本地的服务方法也须要声明为Tcc服务,有三个约束:
(1)在服务方法上加上@Compensable注解,并设置注解属性
(2)服务方法的入参都须能序列化(实现Serializable接口)
(3)try方法、confirm方法和cancel方法入参类型须同样
调用Tcc服务示例:
try方法:
@Compensable(confirmMethod
= "confirmMakePayment", cancelMethod =
"cancelMakePayment")
public void makePayment(Order order,
BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {
System.out.println("order try make payment called.time seq:"
+ DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd
HH:mm:ss"));
order.pay(redPacketPayAmount, capitalPayAmount);
orderRepository.updateOrder(order);
String result = capitalTradeOrderService.record(buildCapitalTradeOrderDto(order));
String result2 =
redPacketTradeOrderService.record(buildRedPacketTradeOrderDto(order));
}
复制代码
confirm方法:
public void confirmMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {}
复制代码
cancel方法:
public void cancelMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {}
复制代码
执行提供的建库建表脚本 (资源文件夹)
tcc-transaction-dubbo-capital 资产服务
tcc-transaction-dubbo-capital-api 资产服务接口
tcc-transaction-dubbo-redpacket 红包服务
tcc-transaction-dubbo-redpacket-api 红包服务接口
tcc-transaction-dubbo-order 订单WEB工程(TCC调用方)
修改sample-dubbo-xxxxx.properties和jdbc.properties