声明: 本篇主要对所用到的技术做了归纳总结,对源码讲解较少,如果有基础的朋友可以直接下载源码结合时序图更能容易理解;基础比较弱的朋友建议先看看资料自看源码这样更容易理解。这里的部分资料来源于网络,所以这里对那些资料提供者表达衷心的感谢。
方案:
- 业务流程:Tss库向Saas转移库存,order为记录表。
- 技术栈: Springboot+mysql+postgreSQL+atomikos+mybatis
- 项目代码地址:https://github.com/bao17634/springboot-kafka-demo.git
1、分布式事务模型 ACID 实现
1.1、X/Open XA 协议(XA)
最早的分布式事务模型是 X/Open 国际联盟提出的 X/Open Distributed Transaction Processing(DTP)模型,也就是大家常说的 X/Open XA 协议,简称XA 协议。
DTP模型如图:
- TM:全局事务管理器
- RM:多个资源管理器
- AP:应用程序
全局事务管理器负责管理全局事务状态与参与的资源,协同资源一起提交或回滚;资源管理器则负责具体的资源操作。
XA 协议主要描述了 TM 与 RM 之间的接口,允许多个资源在同一分布式事务中访问。
基于 DTP 模型的分布式事务流程大致如下:
XA接口详解
- XA接口时双向的系统接口,用于事务管理器和多个资源管理器之间形成桥梁。
- 事务管理器控制JTA事务,管理事务生命周期,并协调资源,在JTA中,事务管理器抽象为TransactionManager接口,并通过底层事务服务JTS(java事务服务)实现,资源管理器负责控制和管理实际资源(如数据库或者JMS(java消息服务)队列)。
下图说明事务管理器、资源管理器,以及JTA环境中客户应用之间的关系:
1.2、阶段提交(2PC)
(1)概念: 二阶段提交(Two-phaseCommit)是指,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm),通常二阶段提交也被称为是一种协议。
(2)二阶段算法思路: 参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
(3)协议假设
- 在分布式系统中,存在一个节点作为协调者,则其他节点作为参与者,且各个节点之间可以进行网络通讯。
- 所有节点都采用预写式日志,切日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的消失。
- 所有节点不会永久损失,即使损坏后任然可以恢复。
参与者与协调者之间的关系如图:
(4)第一阶段(提交请求阶段)
- 协调者节点向所有参与者节点询问是否可以执行提交操作,并开始等待各参与者节点的响应。
- 参与者节点执行询问发起为止的所有事务操作,并将信息写入日志。
- 各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个"同意"消息;如果参与者节点的事务操作实际执行失败,则它返回一个"中止"消息。
(5)第二阶段(提交执行阶段)
- 成功:
当协调者节点从所有参与者节点获得的相应消息都为"同意"时:
- 协调者节点向所有参与者节点发出"正式提交"的请求。
- 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
- 参与者节点向协调者节点发送"完成"消息。
- 协调者节点收到所有参与者节点反馈的"完成"消息后,完成事务。
- 失败
如果任一参与者节点在第一阶段返回的响应消息为"终止",或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
- 协调者节点向所有参与者节点发出"回滚操作"的请求。
- 参与者节点利用之前写入的日志信息执行回滚,并释放在整个事务期间内占用的资源。
- 参与者节点向协调者节点发送"回滚完成"消息。
- 协调者节点收到所有参与者节点反馈的"回滚完成"消息后,取消事务。
(6)用事务来解释二阶段
所谓的二阶段时将提交过程分为两个阶段:
- 1.准备阶段
- 2.提交阶段
(7)JTA(Java Transaction API)与atomikos实现分布式事务主要代码如下:
- 1> 配置数据源: DruidXADataSource 连接池实现了XAResource接口用来进行对资源操作
@Bean(name = "systemDataSource")@Primarypublic
DataSource systemDataSource(){
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setXaProperties(PojoUtil.obj2Properties(postgreSqlProperties));
ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
ds.setUniqueResourceName("systemDataSource");
ds.setPoolSize(5);
ds.setTestQuery("SELECT 1");
return ds;
}
- 2> 配置事务: 配置spring的JtaTransactionManager,其底层交给atomikos进行事务处理。
@Bean(name = "transactionManager")
@DependsOn({"userTransaction","atomikosTransactionManager"})
public PlatformTransactionManager transactionManager() throws Throwable {
UserTransaction userTransaction = userTransaction();
TransactionManager atomikosTransactionManager = atomikosTransactionManager();
JtaTransactionManager jtaTransactionManager = new
JtaTransactionManager(userTransaction, atomikosTransactionManager);
jtaTransactionManager.setAllowCustomIsolationLevels(true);
return jtaTransactionManager;
}
2、@Transactional注解式事务详解
- spring事务管理是指在业务代码在 出现异常之后,对之前的操作进行回滚,保证数据库数据的一致性。
2.1、配置式、注解式事务管理。
(1) 编程式:
TransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def)
try{
businessLogic();
transactionManager.commit(status);
}catch(Exception ex) {
transactionManager. rollback(status);
throw ex;
}
(2)xml配置式
<tx:advice id="txAdvice" transaction-manager="txManager"><tx:attributes>
<tx:method name="create*" propagation="REQUIRED" timeout="300" rollback-for="java.lang.Exception" />
<tx:method name="delete*" propagation="REQUIRED" timeout="300" rollback-for="java.lang.Exception" />
<tx:method name="update*" propagation="REQUIRED" timeout="300" rollback-for="java.lang.Exception" />
<tx:method name="get*" propagation="REQUIRED" read-only="true" timeout="300" />
<tx:method name="*" propagation="REQUIRED" read-only="true" timeout="300" /></tx:attributes></tx:advice>
<aop:config><aop:pointcut id="txPointcut" expression="execution(* com.mico.emptyspring.service.*ServiceA.*(..))" /><aop:advisor pointcut-ref="txPointcut" advice-ref="txAdvice" /></aop:config>
(3)注解式
在配置文件中添加配置:<tx:annotation-driven transaction-manager="txManager"/>
或者transaction-manager="txManager
接着在需要事务的方法或类上面添加@Transactional
注解。
2.2、@Transactional配置项
属性 | 类型 | 描述 |
---|---|---|
value | String | 可选的限定描述符,指定使用的事务管理器 |
propagation | enum: Propagation | 可选的事务传播行为设置 |
isolation | enum:Isolation | 可选的事务隔离级别设置 |
readOnly | boolean | 读写或只读事务,默认读写 |
timeout | int(in seconds granularity) | 事务超时时间设置 |
rollbackFor | Class对象数组,必须继承自Throwable | 导致事务回滚的异常类数组 |
rollbackForClassName | 类名数组,必须继承自Throwable | 导致回滚的异常类名字数组 |
noRollbackFor | Class对象数组,必须继承自Throwable | 不会导致事务回滚的异常类数组 |
noRollbackForClassName | 类名数组,必须继承自Throwable | 不会导致事务回滚的异常类名字数组 |
- value主要是用来指定不同的事务管理器;主要用来满足在同一个系统中,存在不同的事务管理器。 比如:在Spring中,声明两种事务管理器txManager1, txManager2,然后用户可以根据这个参数来根据需要指定特定的txManager。在一个系统中,需要访问多个数据源或者多个数据库,则必然会配置多个事务管理器。
3、事务隔离级别
3.1、事务的隔离级别(由高到低)
Read uncommitted(读未提交)
- 所有事务都可以看到其他未提交事务的执行结果。通俗地讲就是,在一个事务中可以读取到另一个事务中新增或修改但未提交的数据。
- 该隔离级别可能导致的问题是脏读。因为另一个事务可能回滚,所有在第一个事务中读取到的数据很可能是无效的脏数据,造成脏读现象。
Read committed(读提交):
- 一个事务从开始直到提交之前,所做的任何修改对其他事务都是不看见的。
- 该隔离界别可能导致的问题是不可重复读,因为两次执行同样的查询,可能得到的不一样的结果。
Repeatable read(重复读):
- 确保一个事务的多个实例在并发读取数据时,会看到同样的数据行。也就是说,可重复读在一个事务读取数据,怎么读都不发生变化,除非提交了该事务,再次进行读取。
- 该隔离级别存在的问题就是幻读。
Serializable(序列化):
- 这是最高的隔离级别
- 他通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。例如:有两个事务都操作同一个数据行,那么这个数据行就会被锁定,只允许先读取/操作到数据行的事务优先操作,只有当事务提交了,数据才会解锁,后一个事务才能操作成功这个数据行,否则只能一直等待。
- 该隔离级别可能导致大量的超市现象和锁竞争。
3.2、隔离级别对并发性能的影响
4、事务的传播属性
4.1、解释: 如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为,即事务方法的嵌套调用会产生事务传播。
- TransactionDefinition.PROPAGATION_REQUIRED:
如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。 - TransactionDefinition.PROPAGATION_REQUIRES_NEW:
创建一个新的事务,如果当前存在事务,则把当前事务挂起。 - TransactionDefinition.PROPAGATION_SUPPORTS:
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 - TransactionDefinition.PROPAGATION_NOT_SUPPORTED:
以非事务方式运行,如果当前存在事务,则把当前事务挂起。 - TransactionDefinition.PROPAGATION_NEVER:
以非事务方式运行,如果当前存在事务,则抛出异常。 - TransactionDefinition.PROPAGATION_MANDATORY:
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 - TransactionDefinition.PROPAGATION_NESTED:
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
4.2、REQUIRED_NEW和NESTED两种不同的传播机制的区别:
(1) REQUIRED_NEW: 事务内部的事务独立运行,在各自的作用域中,可以独立的回滚或者提交;而外部的事务将不受内部事务的回滚状态影响.启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行.
(2)NESTED: 事务基于单一的事务来管理,提供了多个保存点。这种多个保存点的机制允许内部事务的变更触发外部事务的回滚。如果外部事务 commit, 嵌套事务也会被 commit;如果外部事务 roll back, 嵌套事务也会被 roll back 。开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交
5、此方案总结
此方案是基于XA协议的两阶段提交方案,这也一种是传统的分布式事务方案。
5.1 存在问题:
- 1) 性能问题: 所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
- 2) 可靠性问题: 如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。
- 3) 数据一致性问题: 在阶段 2 中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致。
优点:
- 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域(其实也不能100%保证强一致),适用于低并发性能场景。
缺点:
- 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。