分布式事务与应用、案例详解

本文从单库DB事务到分布式事务理论和应用场景角度出发,分析,实例、整理

1、单库DB事务特性(ACID)

 原子性(A)
所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。
 一致性(C)
事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。
 隔离性(I)
所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。
 持久性(D)

所谓的持久性,就是说一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。

2、分布式事务是啥

    分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性

 2.1  分布式事务产生背景

 背景1  数据库分库分表

      当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,具体分库分表的原理在此不做解释,以后有空详细说,简单的说就是原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。



背景2 应用SOA化


 
  所谓的SOA化,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务

综上二种就是事务操作从原来的单库变成了多库,因此多库的操作依然要符合ACID原则,接下来我们分析具体场景和解决方案

3、 分布式事务应用场景
场景1  支付

最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,要么全部成功,要么全部失败。而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务。

场景2  在线下单
买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性

4、分布式事务解决方案

4.1、基于XA协议的两阶段提交
XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。XA实现分布式事务的原理如下:



总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘

4.2、TCC编程模式:变种两阶段提交

    所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用

4.3、消息事务&最终一致性:变种两阶段提交
    所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:


1、A系统向消息中间件发送一条预备消息
2、消息中间件保存预备消息并返回成功
3、A执行本地事务

4、A发送提交消息给消息中间件

通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:
步骤一出错,则整个事务失败,不会执行A的本地操作
步骤二出错,则整个消息失败,不会执行A的本地操作
步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息
步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?不需要回滚A,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务
   基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。原理如下:


虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。

分布式事务,本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括上面提到的消息事务+最终一致性、TCC模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景

5、Atomikos 生产应用

Atomikos TransactionsEssentials 是一个为Java平台提供增值服务的并且开源类事务管理器(spring、tomcat、JTA),以下是包括在这个开源版本中的一些功能:

l  全面崩溃 / 重启恢复
l  兼容标准的SUN公司JTA API
l  嵌套事务
l  为XA和非XA提供内置的JDBC适配器
注释:XA:XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。目前,Oracle、Informix、DB2和Sybase等各大数据库厂家都提供对XA的支持。XA协议采用两阶段提交方式来管理分布式事务。XA接口提供资源管理器与事务管理器之间进行通信的标准接口。XA协议包括两套函数,以xa_开头的及以ax_开头的。
以下的函数使事务管理器可以对资源管理器进行的操作:
  1)xa_open,xa_close:建立和关闭与资源管理器的连接。
  2)xa_start,xa_end:开始和结束一个本地事务。
  3)xa_prepare,xa_commit,xa_rollback:预提交、提交和回滚一个本地事务。
  4)xa_recover:回滚一个已进行预提交的事务。
  5)ax_开头的函数使资源管理器可以动态地在事务管理器中进行注册,并可以对XID(TRANSACTION IDS)进行操作。
  6)ax_reg,ax_unreg;允许一个资源管理器在一个TMS(TRANSACTION MANAGER SERVER)中动态注册或撤消注册。
l  内置的JMS适配器XA-capable JMS队列连接器
注释:JMS:jms即Java消息服务(Java Message Service)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。
l  通过XA API兼容第三方适配器
l  更好的整合您的项目
l  集成Hibernate

案例1

1、配置数据源 

    这一步是比较重要的。要用AtomikosDataSourceBean,而不是以前用的连接池如dbcp。最好也用XA,注意jdbc的链接地址和登陆账号与普通连接池的配置的格式不一样。下面是一个mysql数据库的配置举例: 

  1. <bean id="dataSource1" class="com.atomikos.jdbc.AtomikosDataSourceBean" init-method="init" destroy-method="close">  
  2.     <property name="uniqueResourceName" value="ds1"/>  
  3.     <property name="xaDataSourceClassName" value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"/>  
  4.     <property name="xaProperties">  
  5.         <props>  
  6.             <prop key="url">jdbc:mysql://localhost/test</prop>  
  7.             <prop key="user">test</prop>  
  8.             <prop key="password">test</prop>  
  9.         </props>  
  10.     </property>  
  11.     <property name="minPoolSize" value="10" />  
  12.     <property name="maxPoolSize" value="100" />  
  13.     <property name="borrowConnectionTimeout" value="30" />  
  14.     <property name="testQuery" value="select 1" />  
  15.     <property name="maintenanceInterval" value="60" />  
  16. </bean>  


再来一个sybase的配置举例: 

  1. <bean id="dataSource2" class="com.atomikos.jdbc.AtomikosDataSourceBean" init-method="init" destroy-method="close">  
  2.     <property name="uniqueResourceName" value="ds2"/>  
  3.     <property name="xaDataSourceClassName" value="com.sybase.jdbc3.jdbc.SybXADataSource"/>  
  4.     <property name="xaProperties">  
  5.         <props>  
  6.             <prop key="serverName">192.168.1.10</prop>  
  7.                         <prop key="portNumber">2638</prop>  
  8.                         <prop key="databaseName">test</prop>  
  9.             <prop key="user">test</prop>  
  10.             <prop key="password">test</prop>  
  11.         </props>  
  12.     </property>  
  13.     <property name="minPoolSize" value="10" />  
  14.     <property name="maxPoolSize" value="100" />  
  15.     <property name="borrowConnectionTimeout" value="30" />  
  16.     <property name="testQuery" value="select 1" />  
  17.     <property name="maintenanceInterval" value="60" />  
  18. </bean>  


2、使用数据源 

mybatis

  1. <bean id="sqlSessionFactory1" class="org.mybatis.spring.SqlSessionFactoryBean">  
  2.     <property name="dataSource" ref="dataSource1"/>  
  3. </bean>  
  4. <bean id="sqlSessionFactory2" class="org.mybatis.spring.SqlSessionFactoryBean">  
  5.     <property name="dataSource" ref="dataSource2"/>  
  6. </bean>  

mybatis还要配置一下映射文件的自动扫描,这里与atomikos无关

  1. <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
  2.     <property name="basePackage" value="xx.xx;" />  
  3.     <property name="sqlSessionFactory" ref="sqlSessionFactory1"/>  
  4. </bean>  
  5. <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
  6.     <property name="basePackage" value="yy.yy;" />  
  7.     <property name="sqlSessionFactory" ref="sqlSessionFactory2"/>  
  8. </bean>  


3、配置jta事务管理 

atomikos注入jta事务

  1. <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">  
  2.     <property name="transactionManager">  
  3.         <bean class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init" destroy-method="close">  
  4.             <property name="forceShutdown" value="true"/>  
  5.         </bean>  
  6.     </property>  
  7.     <property name="userTransaction">  
  8.         <bean class="com.atomikos.icatch.jta.UserTransactionImp"/>  
  9.     </property>  
  10. </bean>  

当然,用spring的声明式事务配置,再加上一行: 

  1. <tx:annotation-driven/>  

(注意,本来要配置transaction-manager属性,如:<tx:annotation-driven transaction-manager="transactionManager"/>。这里没有配置是因为它的默认值是transactionManager) 

4、jta.properties 
    这个文件一般放在根路径吧,与log4j.properties类似。jta.properties也可命名为transactions.properties。如果不配置这个文件,项目也能启动,因为几乎所有配置项都有默认值。最好还是配置了,
详细配置信息请查看:http://www.atomikos.com/Documentation/JtaProperties


案例2 

Atomikos使用非XA数据库驱动实现分布式事务

地址:https://my.oschina.net/pingpangkuangmo/blog/423210

源码:https://gitee.com/pingpangkuangmo/framework/tree/master/jta/spring-jta-atomikos-xa


猜你喜欢

转载自blog.csdn.net/luozhonghua2014/article/details/80037720