参考:
深入浅出SpringBoot2.x
在Spring中,数据库事务是通过AOP技术来提供服务的。
图6-1 执行SQL事务流程
看到Spring AOP的约定,它会把我们的代码织入约定的流程中。同样地,使用AOP的思维后,执行SQL的代码就可以织入Spring约定的数据库事务的流程中。
在讲解Spring AOP时,只要我们遵循约定,就可以把自己开发的代码织入约定的流程中。为了“擦除”令人厌烦的try…catch…finally…语句,减少那些数据库连接开闭和事务回滚提交的代码,Spring利用其AOP为我们提供了一个数据库事务的约定流程。
通过这个约定流程就可以减少大量的冗余代码和一些没有必要的try…catch…finally…语句,让开发者能够更加集中于业务的开发,而不是数据库连接资源和事务的功能开发,这样开发的代码可读性就更高,也更好维护。
对于事务,需要通过标注告诉Spring在什么地方启用数据库事务功能。
对于声明式事务,是使用@Transactional进行标注的。
用法:
内部属性:
这些配置内容是[2.1 @Transactional的作用](#2.1 @Transactional的作用),是在Spring IoC容器在加载时就会将这些配置信息解析出来,然后把这些信息存到事务定义器(TransactionDefinition接口的实现类)里,并且记录哪些类或者方法需要启动事务功能,采取什么策略去执行事务。这个过程中,我们所需要做的只是给需要事务的类或者方法标注@Transactional和配置其属性而已,并不是很复杂。
有了@Transactional的配置,Spring就会知道在哪里启动事务机制,其约定流程如图6-2所示。
图6-2 Spring数据库事务约定
因为这个约定非常重要,所以这里做进一步的讨论。当Spring的上下文开始调用被@Transactional标注的类或者方法时,Spring就会产生AOP的功能。请注意事务的底层需要启用AOP功能,这是Spring事务的底层实现,后面我们会看到一些陷阱。那么当它启动事务时,就会根据事务定义器内的配置去设置事务,首先是根据传播行为去确定事务的策略。有关传播行为后面我们会再谈,这里暂且放下。然后是隔离级别、超时时间、只读等内容的设置,只是这步设置事务并不需要开发者完成,而是Spring事务拦截器根据@Transactional配置的内容来完成的。
在上述场景中,Spring通过对注解@Transactional属性配置去设置数据库事务,跟着Spring就会开始调用开发者编写的业务代码。执行开发者的业务代码,可能发生异常,也可能不发生异常。在Spring数据库事务的流程中,它会根据是否发生异常采取不同的策略。
如果都没有发生异常,Spring数据库拦截器就会帮助我们提交事务,这点也并不需要我们干预。如果发生异常,就要判断一次事务定义器内的配置,如果事务定义器已经约定了该类型的异常不回滚事务就提交事务,如果没有任何配置或者不是配置不回滚事务的异常,则会回滚事务,并且将异常抛出,这步也是由事务拦截器完成的。
无论发生异常与否,Spring都会释放事务资源,这样就可以保证数据库连接池正常可用了,这也是由Spring事务拦截器完成的内容。
在上述场景中,我们还有一个重要的事务配置属性没有讨论,那就是传播行为。它是属于事务方法之间调用的行为,后面我们会对其做更为详细的讨论。但是无论怎么样,从流程中我们可以看到开发者在整个流程中只需要完成业务逻辑即可,其他的使用Spring事务机制和其配置即可,这样就可以把try…catch…finally…、数据库连接管理和事务提交回滚的代码交由Spring拦截器完成,而只需要完成业务代码即可。
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao = null;
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1)
public int insertUser(User user) {
return userDao.insertUser(user);
}
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1)
public User getUser(Long id) {
return userDao.getUser(id);
}
}
上述的事务流程中,事务的打开、回滚和提交是由事务管理器来完成的。在Spring中,事务管理器的顶层接口为PlatformTransactionManager,Spring还为此定义了一些列的接口和类,如图6-3所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iv14Fm1m-1624934864110)(SpringSpring数据库事务处理.assets/clip_image001.png)]
图6-3 Spring事务管理器
因为本书会以MyBatis框架去讨论Spring数据库事务方面的问题,最常用到的事务管理器是DataSourceTransactionManager。
Spring在事务管理时,就是将这些方法按照约定织入对应的流程中的,其中getTransaction方法的参数是一个事务定义器(TransactionDefinition),它是依赖于我们配置的@Transactional的配置项生成的,于是通过它就能够设置事务的属性了,而提交和回滚事务也就可以通过commit和rollback方法来执行。
在Spring Boot中,当你依赖于mybatis-spring-boot-starter之后,它会自动创建一个DataSource- TransactionManager对象,作为事务管理器,如果依赖于spring-boot-starter-data-jpa,则它会自动创建JpaTransactionManager对象作为事务管理器,所以我们一般不需要自己创建事务管理器而直接使用它们即可。
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1)
public int insertUser(User user) {
return userDao.insertUser(user);
}
好处:
#隔离级别数字配置的含义:
#-1 数据库默认隔离级别
#1 未提交读
#2 读写提交
#4 可重复读
#8 串行化
#tomcat数据源默认隔离级别
spring.datasource.tomcat.default-transaction-isolation=2
#dbcp2数据库连接池默认隔离级别
#spring.datasource.dbcp2.default-transaction-isolation=2
传播行为是方法之间调用事务采取的策略问题。
在绝大部分的情况下,我们会认为数据库事务要么全部成功,要么全部失败。但现实中也许会有特殊的情况。例如,执行一个批量程序,它会处理很多的交易,绝大部分交易是可以顺利完成的,但是也有极少数的交易因为特殊原因不能完成而发生异常,这时我们不应该因为极少数的交易不能完成而回滚批量任务调用的其他交易,使得那些本能完成的交易也变为不能完成了。此时,我们真实的需求是,在一个批量任务执行的过程中,调用多个交易时,如果有一些交易发生异常,只是回滚那些出现异常的交易,而不是整个批量任务,这样就能够使得那些没有问题的交易可以顺利完成,而有问题的交易则不做任何事情。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mmiB97Bv-1624934864111)(SpringSpring数据库事务处理.assets/clip_image001.jpg)]
图6-5 事务的传播行为
在Spring中,当一个方法调用另外一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为。
这样讲还是有点抽象,我们再回到图6-5中。图中,批量任务我们称之为当前方法,那么批量事务就称为当前事务,当它调用单个交易时,称单个交易为子方法,当前方法调用子方法的时候,让每一个子方法不在当前事务中执行,而是创建一个新的事务去执行子方法,我们就说当前方法调用子方法的传播行为为新建事务。此外,还可能让子方法在无事务、独立事务中执行,这些完全取决于你的业务需求。
在Spring事务机制中对数据库存在7种传播行为,它是通过枚举类Propagation定义的。下面先来研究它的源码,如代码清单6-16所示。
以上代码中加入中文注释解释了每一种传播行为的含义。传播行为一共分为7种,但是常用的只有代码清单中加粗的3种,其他的使用率比较低。
NESTED传播行为与REQUIRES_NEW
在大部分的数据库中,一段SQL语句中可以设置一个标志位,然后后面的代码执行时如果有异常,只是回滚到这个标志位的数据状态,而不会让这个标志位之前的代码也回滚。这个标志位,在数据库的概念中被称为保存点(save point)。从加粗日志部分可以看到,Spring为我们生成了nested事务,而从其日志信息中可以看到保存点的释放,可见Spring也是使用保存点技术来完成让子事务回滚而不致使当前事务回滚的工作。注意,并不是所有的数据库都支持保存点技术,因此Spring内部有这样的规则:当数据库支持保存点技术时,就启用保存点技术;如果不能支持,就新建一个事务去运行你的代码,即等价于REQUIRES_NEW传播行为。NESTED传播行为和REQUIRES_NEW还是有区别的。NESTED传播行为会沿用当前事务的隔离级别和锁等特性,而REQUIRES_NEW则可以拥有自己独立的隔离级别和锁等特性,这是在应用中需要注意的地方。
我们谈过Spring数据库事务的约定,其实现原理是AOP,而AOP的原理是动态代理,在自调用的过程中,是类自身的调用,而不是代理对象去调用,那么就不会产生AOP,这样Spring就不能把你的代码织入到约定的流程中,于是就产生了现在看到的失败场景。
为了克服这个问题,我们可以像6.4.2节那样,