Spring中使用事务
Spring是一个伟大的框架,从一开始只是一个容器框架,到现在已经发展成为了一个包含企业开发中的方方面面的很多框架的总称。它不但从复杂度上,发展出了用于各个方面的子框架。它还从易用性出发,推出了像Spring-Boot这样的框架,使得搭建环境变得异常的简单。
很早之前Spring就已经有了一套自己的事务规范。(在org.springframework.transaction包中),而且用起来也非常的简单:
@Service
public Class OrderService {
@Transactional
public TicketOrder buyTicket(OrderDTO orderDTO) {
TicketOrder tkOrder = new TicketOrder();
jdbcTemplate.execute(createOrderSQL);
return tkOrder;
}
}
我们只需要在方法上加一个 Transactional 标签,那个这个方法就会在一个事务里面执行。这是用代理模式实现的。Spring 容器在初始化这个 service 实例的时候,实际上是创建一个代理类,然后在调用这个方法的时候,包装一个事务的处理。上面的方式使用代理模式展开,大致如下:
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,所有的数据修改又回到未修改状态。
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 的 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) 的资源,如:
- JDBC Connection: JDBC 的连接支持 commit () 和 rollback () 操作。。
- JPA 的 Entity: 从 JPA 的 EntityManager 中获得 EntityTransaction,通过它实现。
- JMS 的 session:jms 的 session 提供 commit () 和 rollback () 操作。
- JTA 的事务:JTA 的事务当然提供了事务的操作。
在微服务情况下,我们经常会遇到使用多个微服务,调用而且需要保证事务一致性的情况。
Spring 中 @Transactional ,默认只对抛出的 RuntimeException 的出常,事务才会回滚。
如果希望无论抛出是 RuntimeException ,还是 Exception,事务都要回滚,请使用如下配置。
@Transactional(rollbackFor={RuntimeException.class, Exception.class})
Transactional为什么不能保证微服务条件下事务?
但是如果发起方挂了,或者网络挂了事务讲无法保证
@Transactional
public TicketOrder buyTicket(OrderDTO orderDTO) {
orderRepository.save(order);
String res = restTemplate.updatexxxx(order);
if (res!="200") {
throw new RuntimeException("xxx");
}
return tkOrder;
}
我们根据updatexxxx的返回值来决定是否抛出异常,如果有异常就回滚。
这种在发起发方正常运作的时候,是可行过是吧的。但是如果发起方挂了或者发起方事务不成功怎么办?,会出现事务不一致的情况,意味着我们需要回滚被调用侧。这意味着我们代码中将需要同时处理两者的事务,并且需要被调用放提供回滚接口。
所以出现了XA规范,把事务管理器给独立出去。
单仔细想下,我们其实可以在本地拿一张表记下本地的事务提交记录,如果提交成功,我们删除这条记录,如果没有提交成功,我们要要调用被调用方的回滚接口。
在Java中,使用了java.sql.Connection实例来表示和数据库的一个连接,通信的方式目前基本上采用的是TCP/IP 连接方式。通过对Connection进行一系列的事务控制。
MVCC(Multi-Version Concurrency Control)
来复习一下mysql是如何实现事务的?MVCC(Multi-Version Concurrency Control)多版本并发控制,是数据库控制并发访问的一种手段。
1. 版本链
版本链是一条链表,链接的是每条数据曾经的修改记录
那么这个版本链又是如何形成的呢,每条数据又是靠什么链接起来的呢?
其实是这样的,对于InnoDB存储引擎的表来说,它的聚簇索引记录包含两个隐藏字段
- trx_id: 存储修改此数据的事务id,只有这个事务操作了某些表的数据后当更改操作发生的时候(update,delete,insert),才会分配唯一的事务id,并且此事务id是递增的
- roll_pointer: 指针,指向上一次修改的记录
- row_id(非必须): 当有主键或者有不允许为null的unique键时,不包含此字段
假如说当前数据库有一条这样的数据,假设是事务ID为100的事务插入的这条数据,那么此条数据的结构如下 - 后来,事务200,事务300,分别来修改此数据
所以此时的版本链如下 - 我们每更改一次数据,就会插入一条undo日志,并且记录的roll_pointer指针会指向上一条记录,如图所示
1、第一条数据是小杰,事务ID为100
2、事务ID为200的事务将名称从小杰改为了A
3、事务ID为200的事务将名称从A又改为了B
4、事务ID为300的事务将名称从B又改为了C
所以串成的链表就是 C -> B -> A -> 小杰 (从最新的数据到最老的数据)
2. 一致性视图(ReadView)
需要判断版本链中的哪个版本是是当前事务可见的,因此有了一致性视图的概念。其中有四个属性比较重要 - m_ids: 在生成ReadView时,当前活跃的读写事务的事务id列表
- min_trx_id: m_ids的最小值
- max_trx_id: m_ids的最大值+1
- creator_trx_id: 生成该事务的事务id,单纯开启事务是没有事务id的,默认为0,creator_trx_id是0。
版本链中的当前版本是否可以被当前事务可见的要根据这四个属性按照以下几种情况来判断 - 当 trx_id = creator_trx_id 时:当前事务可以看见自己所修改的数据, 可见,
- 当 trx_id < min_trx_id 时 : 生成此数据的事务已经在生成readView前提交了, 可见
- 当 trx_id >= max_trx_id 时 :表明生成该数据的事务是在生成ReadView后才开启的, 不可见
- 当 min_trx_id <= trx_id < max_trx_id 时
- trx_id 在 m_ids 列表里面 :生成ReadView时,活跃事务还未提交,不可见
- trx_id 不在 m_ids 列表里面 :事务在生成readView前已经提交了,可见
如果某个版本数据对当前事务不可见,那么则要顺着版本链继续向前寻找下个版本,继续这样判断,以此类推。
注:RR和RC生成一致性视图的时机不一样 (这也是两种隔离级别实现的主要区别)
读提交(read committed RC) 是在每一次select的时候生成ReadView的 可重复读(repeatable read
RR)是在第一次select的时候生成ReadView的
那么我们知道了mysql数据库是根据事务id,只有知道事务id,才可以找到undolog实现,而事务id是被数据库连接管理的,数据库连接又是关联到我们应用线程的。 分布式环境中,我们只要知道一个发起方的全局事务id,然后在上下文调用链中传递,每在被调用中开启一个事务,就绑定一个分支事务id,一个全局事务可能包含多个事务分支,那么我们就能追踪所有的分布式事务,并控制其状态。
两阶段提交协议
阿里的seata框架很好地完成了这一工作
官网:https://seata.io/zh-cn/index.html
阿里GTS:https://help.aliyun.com/document_detail/157850.html
分布式事务包含以下 3 个核心组件:
- Transaction Coordinator(TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
- Transaction Manager(TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
- Resource Manager(RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
一个典型的事务过程包括:
1、TM 向 TC 申请开启(Begin)一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
2、XID 在微服务调用链路的上下文中传播。
3、RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
4、TM 向 TC 发起针对 XID 的全局提交(Commit)或回滚(Rollback)决议。
5、TC 调度 XID 下管辖的全部分支事务完成提交(Commit)或回滚(Rollback)请求。
事务框架
基于架构上定义的 3 个核心组件,分布式事务被抽象成如下事务框架。
3个核心组件的功能如下:
- TM定义全局事务的边界。
- RM负责定义分支事务的边界和行为。
- TC、TM和RM交互,做全局的协调。交互包括开启(Begin)、提交(Commit)、回滚(Rollback)全局事务;分支注册(Register Branch)、状态上报(Branch Status Report)和分支提交(Branch Commit)、分支回滚(Branch Rollback)。
事务模式
事务模式是这个框架下 RM 驱动的分支事务的不同行为模式,即事务(分支)模式。事务模式包括 AT 模式、TCC 模式、Saga 模式和 XA 模式。
AT 模式 RM 驱动分支事务的行为分为以下两个阶段:
- 执行阶段:
1、代理 JDBC 数据源,解析业务 SQL,生成更新前后的镜像数据,形成 UNDO LOG。
2、向 TC 注册分支。
1、分支注册成功后,把业务数据的更新和 UNDO LOG 放在同一个本地事务中提交。 - 完成阶段:
1、全局提交,收到 TC 的分支提交请求,异步删除相应分支的 UNDO LOG。
2、全局回滚,收到 TC 的分支回滚请求,查询分支对应的 UNDO LOG 记录,生成补偿回滚的 SQL 语句,执行分支回滚并返回结果给 TC。
TCC模式
TCC 模式 RM 驱动分支事务的行为分为以下两个阶段:
- 执行阶段:
1、向 TC 注册分支。
2、执行业务定义的 Try 方法。
3、向 TC 上报 Try 方法执行情况:成功或失败。 - 完成阶段:
1、全局提交,收到 TC 的分支提交请求,执行业务定义的 Confirm 方法。
2、全局回滚,收到 TC 的分支回滚请求,执行业务定义的 Cancel 方法。 - Saga 模式 RM 驱动分支事务的行为包含以下两个阶段:
- 执行阶段:
1、向 TC 注册分支。
2、执行业务方法。
3、 向 TC 上报业务方法执行情况:成功或失败。 - 完成阶段:
1、全局提交,RM 不需要处理。
2、全局回滚,收到 TC 的分支回滚请求,执行业务定义的补偿回滚方法。
XA 模式
XA 模式 RM 驱动分支事务的行为包含以下两个阶段:
- 执行阶段:
1、向 TC 注册分支。
2、XA Start,执行业务 SQL,XA End。
3、XA prepare,并向 TC 上报 XA 分支的执行情况:成功或失败。 - 完成阶段:
1、收到 TC 的分支提交请求,XA Commit。
2、收到 TC 的分支回滚请求,XA Rollback。
当然还有其它很多分布式框架如Atomikos,是分布式刚性事务的一种解决方案,但是不推荐。通常情况下使用2pc提交方案的场景都是单服务多数据源(多数据库)的情况。大名dingding的分库分表中间件ShardingSphere 也集成了 SEATA作为分布式事务解决方案
以上的都是基于同步机制,对业务都有一定侵入,并且使用rpc或者是rest进行通信,这种性能有限。所以出现了利用消息中间件来处理分布式事务的方式:通知型事务,我们只需要保证消息的投递是事务的即可。通知型事务分:MQ事务消息、最大努力通知型。
事务消息
异步确保型事务:主要适用于内部系统的数据最终一致性保障,因为内部相对比较可控,如订单和购物车、收货与清算、支付与结算等等场景;
最大努力通知:主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知实现数据最终一致性,比如充值平台与运营商、支付对接等等跨网络系统级别对接;
(1)本地消息表
这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。它和MQ事务消息的实现思路都是一样的,都是利用MQ通知不同的服务实现事务的操作。不同的是,针对消息队列的信任情况,分成了两种不同的实现。本地消息表它是对消息队列的稳定性处于不信任的态度,认为消息可能会出现丢失,或者消息队列的运行网络会出现阻塞,于是在数据库中建立一张独立的表,用于存放事务执行的状态,配合消息队列实现事务的控制。
(2)MQ事务消息
有一些第三方的MQ是支持事务消息的,比如RocketMQ,ActiveMQ,他们支持事务消息的方式也是类似于采用的二阶段提交。 以RocketMQ中间件为例,其思路大致为:
第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务。
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了 RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
RocketMQ事务消息的执行流程图
总结:
seata是实现分布式事务的不错选择。一般需要单独部署服务,并且保证其高可用。
官网:https://seata.io/zh-cn/index.html
https://seata.io/zh-cn/docs/overview/what-is-seata.html 源码:https://github.com/seata/seata
Demo:https://github.com/seata/seata-samples
(1)TCC方案
特点:严格一致性 执行时间短 实时性要求高
适用场景:抢红包 实时转账汇款 收付款
(2)异步确保方案
特点:实时性不高 执行周期较长
适用场景:非实时汇款 退货退款业务
(3)最大努力通知方案
特点:高并发低耦合 不支持回滚
适用场景:获取交易结果(例如:共享单车支付等)
对于事务要求不高的,也可使用消息方式。
参考:
https://mp.weixin.qq.com/s/nROY4rFH8SgnP2kYkuww8g
https://www.likecs.com/show-305819252.html https://seata.io/zh-cn/index.html