目录
一、Spring事务基础概念
1、Spring和事务的关系
- 事务性资源:如关系型数据库Mysql、Oracle、某些消息队列中间件等,因为它们本身支持事务,也能够处理事务。
- Spring不是事务性资源,但是它可以管理事务性资源,所以Spring和事务之间是管理关系。
2、Spring事务三要素
- 数据源:表示具体的事务性资源,是事务的真正处理者,如MySQL等。
- 事务管理器:像一个大管家,从整体上管理事务的处理过程,如打开、提交、回滚等。
- 事务应用和属性配置:像一个标识符,表明哪些方法要参与事务,如何参与事务,以及一些相关属性如隔离级别、超时时间等。
3、事务的基本原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
- 获取连接
Connection con = DriverManager.getConnection()
- 开启事务
con.setAutoCommit(true/false);
- 执行CRUD
- 提交事务/回滚事务
con.commit() / con.rollback();
- 关闭连接
conn.close();
使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。
4、Spring事务配置
SpringBoot 使用事务非常简单,首先使用注解 @EnableTransactionManagement 开启事务支持后,然后在访问数据库的Service方法上添加注解 @Transactional 便可。
以注解方式为例子:
1、配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
2、spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
3、真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
二、事务类型
分为两种:编程式事务、声明式事务
(1)编程式事务
编程式事务管理使用
TransactionTemplate
或者直接使用底层的PlatformTransactionManager
。对于编程式事务管理,spring推荐使用TransactionTemplate
。很少用,但是可以看下官网的例子:
public class SimpleService implements Service {
private final TransactionTemplate transactionTemplate;
// 使用构造对transactionTemplate进行初始化
// 需要提供一个transactionManager
public SimpleService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public Object someServiceMethod() {
return transactionTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
// 这里实现自己的相关业务逻辑
updateOperation1();
return resultOfUpdateOperation2();
}
});
}
}
在上面的例子中,显示的使用了 TransactionTemplate 来完成事务管理,通过实现 TransactionCallback 接口并在其 doInTransaction 方法中完成了我们对业务的处理。我们可以大概看下 TransactionTemplate的execute 方法的实现,实际上就是通过一个 TransactionCallback 封装了业务逻辑,然后 TransactionTemplate 会在事务的上下文中调用:
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
// 1.通过事务管理器开启事务
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
// 2.执行传入的业务逻辑
result = action.doInTransaction(status);
}
catch (RuntimeException | Error ex) {
// 3.出现异常,进行回滚
rollbackOnException(status, ex);
throw ex;
}
catch (Throwable ex) {
// 3.出现异常,进行回滚
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex,
"TransactionCallback threw undeclared checked exception");
}
// 4.正常执行完成的话,提交事务
this.transactionManager.commit(status);
return result;
}
}
(2)声明式事务
声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理也有两种常用的方式,一种是基于xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。
三、事务的隔离级别
1.首先说明一下事务并发引起的三种情况:
1) Dirty Reads 脏读
一个事务正在对数据进行更新操作,但是更新还未提交,另一个事务这时也来操作这组数据,并且读取了前一个事务还未提交的数据,而前一个事务如果操作失败进行了回滚,后一个事务读取的就是错误数据,这样就造成了脏读。
2) Non-Repeatable Reads 不可重复读
一个事务多次读取同一数据,在该事务还未结束时,另一个事务也对该数据进行了操作,而且在第一个事务两次次读取之间,第二个事务对数据进行了更新,那么第一个事务前后两次读取到的数据是不同的,这样就造成了不可重复读。
3) Phantom Reads 幻像读
第一个数据正在查询符合某一条件的数据,这时,另一个事务又插入了一条符合条件的数据,第一个事务在第二次查询符合同一条件的数据时,发现多了一条前一次查询时没有的数据,仿佛幻觉一样,这就是幻像读。
非重复度和幻像读的区别:
非重复读是指同一查询在同一事务中多次进行,由于其他提交事务所做的修改或删除,每次返回不同的结果集,此时发生非重复读。
幻像读是指同一查询在同一事务中多次进行,由于其他提交事务所做的插入操作,每次返回不同的结果集,此时发生幻像读。
表面上看,区别就在于非重复读能看见其他事务提交的修改和删除,而幻像能看见其他事务提交的插入。
2.隔离级别:
1) DEFAULT (默认)
这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。
2) READ_UNCOMMITTED (读未提交)
这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。
3) READ_COMMITTED (读已提交)
保证一个事务修改的数据提交后才能被另外一个事务读取,另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。
4) REPEATABLE_READ (可重复读)
这种事务隔离级别可以防止脏读、不可重复读,但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了不可重复读。
5) SERIALIZABLE(串行化)
这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。除了防止脏读、不可重复读外,还避免了幻像读。
四、事务的传播行为(传播属性)
1) REQUIRED(默认属性)
如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
被设置成这个级别时,会为每一个被调用的方法创建一个逻辑事务域。如果前面的方法已经创建了事务,那么后面的方法支持当前的事务,如果当前没有事务会重新建立事务。
2) MANDATORY
支持当前事务,如果当前没有事务,就抛出异常。
3) NEVER
以非事务方式执行,如果当前存在事务,则抛出异常。
4) NOT_SUPPORTED
以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
5) REQUIRES_NEW
新建事务,如果当前存在事务,把当前事务挂起。
6) SUPPORTS
支持当前事务,如果当前没有事务,就以非事务方式执行。
7) NESTED
支持当前事务,新增Savepoint点,与当前事务同步提交或回滚。
嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:
它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。
使用PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA 事务管理器的支持。
使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。
五、事务失效
1.@Transactional失效场景
(1)底层数据库引擎不支持事务
如果数据库引擎不支持事务,则Spring自然无法支持事务。
(2)在非public修饰的方法使用
@Transactional注解使用的是AOP,在使用动态代理的时候只能针对
public
方法进行代理,源码依据在AbstractFallbackTransactionAttributeSource
类中的computeTransactionAttribute
方法中,如下:
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
此处如果不是标注在
public
修饰的方法上并不会抛出异常,但是会导致事务失效。
(3)异常被 " 踹死了 "
这种情况小白是最容易犯错的,在整个事务的方法中使用
try-catch
,导致异常无法抛出,自然会导致事务失效。伪代码如下:
@Transactional
public void method(){
try{
//插入一条数据
//更改一条数据
}catch(Exception ex){
return;
}
}
(4)方法中调用同类的方法
简单的说就是一个类中的
A方法
(未标注声明式事务)在内部调用了B方法
(标注了声明式事务),这样会导致B方法中的事务失效。代码如下:
public class Test{
public void A(){
//插入一条数据
//调用B方法
B();
}
@Transactional
public void B(){
//插入数据
}
}
为什么会失效呢?:其实原因很简单,Spring在扫描Bean的时候会自动为标注了
@Transactional
注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B()
,此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。
(5)rollbackFor属性设置错误
很容易理解,指定异常触发回滚,一旦设置错误,导致一些异常不能触发回滚,此时的声明式事务不就失效了吗。
默认情况下
@Transactional
注解会对unchecked异常进行回滚,对checked不进行回滚。那什么事unchecked,什么事checked?通俗的说,编译器能检测到的是checked,检测不到的是unchecked
派出于Error或者RuntimeException(比如空指针,1/0)的异常为unchecked异常
继承自Exception的异常统称为checked异常,如IOException、TimeoutException
(6)noRollbackFor属性设置错误
抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。
(7)propagation属性设置错误
事务的传播属性在上面已经介绍了,默认的事务传播属性是
Propagation.REQUIRED
,但是一旦配置了错误的传播属性,也是会导致事务失效,如下三种配置将会导致事务失效:
Propagation.SUPPORTS
Propagation.NOT_SUPPORTED
Propagation.NEVER
(8)原始SSM项目,重复扫描导致事务失效
在原始的SSM项目中都配置了
context:component-scan
并且同时扫描了service层,此时事务将会失效。按照Spring配置文件的加载顺序来说,会先加载Springmvc的配置文件,如果在加载Springmvc配置文件的时候把service也加载了,但是此时事务还没加载,将会导致事务无法成功生效。
解决方法很简单,把扫描service层的配置设置在Spring配置文件或者其他配置文件中即可。
六、事务失效源码分析
1.失效场景
第一种
Transactional注解标注方法修饰符为非public时,@Transactional注解将会不起作用。例如以下代码。
定义一个错误的@Transactional标注实现,修饰一个默认访问符的方法
@Component
public class TestServiceImpl {
@Resource
TestMapper testMapper;
@Transactional
void insertTestWrongModifier() {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
}
在同一个包内,新建调用对象,进行访问。
@Component
public class InvokcationService {
@Resource
private TestServiceImpl testService;
public void invokeInsertTestWrongModifier(){
//调用@Transactional标注的默认访问符方法
testService.insertTestWrongModifier();
}
}
测试用例
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Resource
InvokcationService invokcationService;
@Test
public void testInvoke(){
invokcationService.invokeInsertTestWrongModifier();
}
}
以上的访问方式,导致事务没开启,因此在方法抛出异常时,testMapper.insert(new Test(10,20,30));操作不会进行回滚。如果
TestServiceImpl#insertTestWrongModifier
方法改为public的话将会正常开启事务,testMapper.insert(new Test(10,20,30));将会进行回滚。
第二种
在类内部调用调用类内部@Transactional标注的方法。这种情况下也会导致事务不开启。示例代码如下。
设置一个内部调用
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Transactional
public void insertTestInnerInvoke() {
//正常public修饰符的事务方法
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
public void testInnerInvoke(){
//类内部调用@Transactional标注的方法。
insertTestInnerInvoke();
}
}
测试用例。
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Resource
TestServiceImpl testService;
/**
* 测试内部调用@Transactional标注方法
*/
@Test
public void testInnerInvoke(){
//测试外部调用事务方法是否正常
//testService.insertTestInnerInvoke();
//测试内部调用事务方法是否正常
testService.testInnerInvoke();
}
}
上面就是使用的测试代码,运行测试知道,外部调用事务方法能够征程开启事务,testMapper.insert(new Test(10,20,30))操作将会被回滚;
然后运行另外一个测试用例,调用一个方法在类内部调用内部被@Transactional标注的事务方法,运行结果是事务不会正常开启,testMapper.insert(new Test(10,20,30))操作将会保存到数据库不会进行回滚。
第三种
事务方法内部捕捉了异常,没有抛出新的异常,导致事务操作不会进行回滚。示例代码如下。
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Transactional
public void insertTestCatchException() {
try {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
//运行期间抛异常
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}catch (Exception e){
System.out.println("i catch exception");
}
}
}
测试用例代码如下。
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Resource
TestServiceImpl testService;
@Test
public void testCatchException(){
testService.insertTestCatchException();
}
}
运行测试用例发现,虽然抛出异常,但是异常被捕捉了,没有抛出到方法 外, testMapper.insert(new Test(210,20,30))操作并没有回滚。
以上三种就是@Transactional注解不起作用,@Transactional注解失效的主要原因。下面结合spring中对于@Transactional的注解实现源码分析为何导致@Transactional注解不起作用。
2.原理分析:
第一种
@Transactional
注解标注方法修饰符为非public时,@Transactional
注解将会不起作用。这里分析 的原因是,@Transactional
是基于动态代理实现的,@Transactional
注解实现原理中分析了实现方法,在bean初始化过程中,对含有@Transactional
标注的bean实例创建代理对象,这里就存在一个spring扫描@Transactional
注解信息的过程,不幸的是源码中体现,标注@Transactional的方法如果修饰符不是public,那么就默认方法的@Transactional
信息为空,那么将不会对bean进行代理对象创建或者不会对方法进行代理调用
@Transactional
注解实现原理中,介绍了如何判定一个bean是否创建代理对象,大概逻辑是。根据spring创建好一个aop切点BeanFactoryTransactionAttributeSourceAdvisor
实例,遍历当前bean的class的方法对象,判断方法上面的注解信息是否包含@Transactional
,如果bean任何一个方法包含@Transactional
注解信息,那么就是适配这个BeanFactoryTransactionAttributeSourceAdvisor切点。则需要创建代理对象,然后代理逻辑为我们管理事务开闭逻辑。spring源码中,在拦截bean的创建过程,寻找bean适配的切点时,运用到下面的方法,目的就是寻找方法上面的@Transactional信息,如果有,就表示切点BeanFactoryTransactionAttributeSourceAdvisor能够应用(canApply)到bean中,
AopUtils#canApply(org.springframework.aop.Pointcut, java.lang.Class<?>, boolean)
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
Assert.notNull(pc, "Pointcut must not be null");
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}
MethodMatcher methodMatcher = pc.getMethodMatcher();
if (methodMatcher == MethodMatcher.TRUE) {
// No need to iterate the methods if we're matching any method anyway...
return true;
}
IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
}
//遍历class的方法对象
Set<Class<?>> classes = new LinkedHashSet<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
classes.add(targetClass);
for (Class<?> clazz : classes) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
for (Method method : methods) {
if ((introductionAwareMethodMatcher != null &&
introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||
//适配查询方法上的@Transactional注解信息
methodMatcher.matches(method, targetClass)) {
return true;
}
}
}
return false;
}
我们可以在上面的方法打断点,一步一步调试跟踪代码,最终上面的代码还会调用如下方法来判断。在下面的方法上断点,回头看看方法调用堆栈也是不错的方式跟踪。
AbstractFallbackTransactionAttributeSource#getTransactionAttribute
AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
// Don't allow no-public methods as required.
//非public 方法,返回@Transactional信息一律是null
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
//后面省略.......
}
不创建代理对象
所以,如果所有方法上的修饰符都是非public的时候,那么将不会创建代理对象。以一开始的测试代码为例,如果正常的修饰符的testService是下面图片中的,经过cglib创建的代理对象。
如果class中的方法都是非public的那么将不是代理对象。
不进行代理调用
考虑一种情况,如下面代码所示。两个方法都被@Transactional注解标注,但是一个有public修饰符一个没有,那么这种情况我们可以预见的话,一定会创建代理对象,因为至少有一个public修饰符的@Transactional注解标注方法。
创建了代理对象,insertTestWrongModifier就会开启事务吗?答案是不会。
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Override
@Transactional
public void insertTest() {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
@Transactional
void insertTestWrongModifier() {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
}
原因是在动态代理对象进行代理逻辑调用时,在cglib创建的代理对象的拦截函数中
CglibAopProxy.DynamicAdvisedInterceptor#intercept
,有一个逻辑如下,目的是获取当前被代理对象的当前需要执行的method适配的aop逻辑。
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
而针对@Transactional注解查找aop逻辑过程,相似地,也是执行一次
AbstractFallbackTransactionAttributeSource#getTransactionAttribute
AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
也就是说还需要找一个方法上的@Transactional注解信息,没有的话就不执行代理@Transactional对应的代理逻辑,直接执行方法。没有了@Transactional注解代理逻辑,就无法开启事务
第二种
在类内部调用调用类内部@Transactional标注的方法。这种情况下也会导致事务不开启。
经过对第一种的详细分析,对这种情况为何不开启事务管理,原因应该也能猜到;
既然事务管理是基于动态代理对象的代理逻辑实现的,那么如果在类内部调用类内部的事务方法,这个调用事务方法的过程并不是通过代理对象来调用的,而是直接通过this对象来调用方法,绕过的代理对象,肯定就是没有代理逻辑了。
其实我们可以这样玩,内部调用也能实现开启事务,代码如下。
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Resource
TestServiceImpl testServiceImpl;
@Transactional
public void insertTestInnerInvoke() {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
public void testInnerInvoke(){
//内部调用事务方法
testServiceImpl.insertTestInnerInvoke();
}
}
上面就是使用了代理对象进行事务调用,所以能够开启事务管理,但是实际操作中,没人会闲的蛋疼这样子玩
第三种
事务方法内部捕捉了异常,没有抛出新的异常,导致事务操作不会进行回滚。
这种的话,可能我们比较常见,问题就出在代理逻辑中,我们先看看源码里卖弄动态代理逻辑是如何为我们管理事务的。
TransactionAspectSupport#invokeWithinTransaction
代码如下。
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
//开启事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
//反射调用业务方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
//异常时,在catch逻辑中回滚事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
//提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
else {
//....................
}
}
所以看了上面的代码就一目了然了,事务想要回滚,必须能够在这里捕捉到异常才行,如果异常中途被捕捉掉,那么事务将不会回滚。