Spring 事务控制及传播
前言
在Spring和MySQL中,事务是一组操作被视为一个单独的工作单元,要么全部成功执行,要么全部失败回滚的机制。在Spring中,事务管理是通过TransactionManager来实现的,可以通过注解(@Transactional)或编程方式(TransactionTemplate)来控制事务的边界。在MySQL中,事务是通过BEGIN、COMMIT和ROLLBACK语句来控制的,可以确保数据库操作的一致性和完整性。通过事务的机制,可以保证数据的一致性,避免数据丢失或不一致的情况发生。
在sql中可以使用 来提交或者回滚事务
start TRANSACTION (开启事务) delete from gpt_user_account_other where openid = '4324354';(sql语句执行) COMMIT / ROLLBACK(控制事务状态);
一、@Transactional 注解解释?
注解中的参数 | 参数解释 |
---|---|
propagation | 事务的传播行为,用于指定在方法调用时,当前方法的事务如何传播到被调用的方法。常用的取值有:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。 |
isolation | 事务的隔离级别,用于指定事务的隔离级别,即多个事务之间的隔离程度。常用的取值有:DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE(需要数据库支持这些值)。 |
timeout | 事务的超时时间,用于指定事务的超时时间,单位为秒。 |
readOnly | 是否为只读事务,用于指定事务是否为只读事务,如果为只读事务,则不允许进行写操作。 |
rollbackFor | 指定哪些异常会触发事务回滚,可以指定一个或多个异常类(默认 RuntimeException及其子类)。 |
noRollbackFor | 指定哪些异常不会触发事务回滚,可以指定一个或多个异常类。 |
transactionManager ,value | 这两个都是用于指定事务管理器的名称,可以指定多个事务管理器。不填都是默认的事务管理器(transactionManager) |
二、@Transactional 事务注解作用域
1.注解作用在类上,表示该类的公共方法在执行的时候,会被事务控制
2.作用在方法上表示该方法在一个事务中执行
3.不建议作用在接口Interface上,建议直接标注在类上
事务失效场景1:如果标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效,抽象类上面标注@Transactional 注解也是无意义的,事务不会生效
事务失效场景2:@Transactional标注的类没有是依赖spring容器管理,事务所在目标,没有进行动态代理会失效
事务失效场景3:@Transactional所修饰的方法,不是public修饰(这里必须是pubilc修饰) 且方法中有static修饰、final修饰,abstract 修饰会失效
三、事务传播机制(声明式事务)
1.REQUIRED
如果当前没有事务,就新建一个事务;如果已经存在一个事务中,加入到这个事务中。这是最常见的传播机制,也是默认的传播机制。
1.方法简单的话,一般将dao层执行的sql放在一个方法内,加一个@Transactional注解,注解中propagation 可以不写,使用默认的传播机制
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
@Override
public void requiredMethod1(RequiredReq req) {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("currentTransactionName:" + currentTransactionName);
//1.插入用户信息表
userAccountOtherMapper.insert(req.getAccountOtherPO());
//2.插入用户余额表
userAccountQuotaMapper.insertAccountQuota(req.getUserAccountQuotaPO());
//3.模拟异常情况,引起事务回滚
int i = 1/0;
}
事务失效场景4:使用try …catch… 捕获异常,下面代码中第一步和第二步的数据正常保存,事务没有回滚,因为spring认为这个代码是正常的
事务失效场景5: 使用@Transactional,未设置回滚异常参数而抛出了 Exception异常,spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。比如常见的IOExeption和SQLException,可以补充参数设置rollbackFor = Exception.class
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
@Override
public void requiredMethod1(RequiredReq req) {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("currentTransactionName:" + currentTransactionName);
try {
//1.插入用户信息表
userAccountOtherMapper.insert(req.getAccountOtherPO());
//2.插入用户余额表
userAccountQuotaMapper.insertAccountQuota(req.getUserAccountQuotaPO());
//模拟异常情况,也可以将第二步的sql故意写错
int i = 1/0;
} catch (Exception e) {
//1.如果不想抛出异常,代码的后续逻辑继续执行,可以使用下面代码(手动回滚)
//TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// throw new RuntimeException(e);
e.printStackTrace();
}
xxxxservice.saverecord()
}
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();开启事务后,可以手动回滚try里面的事务
事务失效场景6:事务传播机制使用错误,在开发中难免会调用别的service层中方法,如果想下面代码第二步和第一步在同一个事务下面必须saveAccountQuota()方法上面补充一个注解,让第二步方法事务加入到requiredMethod2()这个事务方法下
@Transactional(rollbackFor = Exception.class)
@Override
public void requiredMethod2(RequiredReq req) {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("currentTransactionName:" + currentTransactionName);
//1.用户基本信息保存
userAccountOtherMapper.insert(req.getAccountOtherPO());
//2.用户账户信息保存
accountQuotaimpl.saveAccountQuota(req.getUserAccountQuotaPO());
}
//这个注解不加,这个方法执行报错,之前的事务不会回滚
@Transactional(rollbackFor = Exception.class)
@Override
public void saveAccountQuota(UserAccountQuotaPO userAccountQuotaPO) {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("currentTransactionNameQUTA:" + currentTransactionName);
userAccountQuotaMapper.insertAccountQuota(userAccountQuotaPO);
}
//这里看到打印结果currentTransactionName 和 currentTransactionNameQUTA 后面的值(事务名称)是一样的
2.SUPPORTS
如果当前有事务,则加入这个事务;如果当前没有事务,则以非事务的方式执行。适用于不需要事务的方法,但是如果有事务的话也可以加入。(用的一般)
适合于希望在有事务的情况下执行方法,但也能够在没有事务的情况下执行方法的场景,一般像这种方法单表操作,你不需要开启一个事务,但是别人调你的方法,他的方法中有事务操作,需要控制事务,可以使用这个传播机制来控制下
@Transactional(rollbackFor = Exception.class,propagation = Propagation.SUPPORTS)
@Override
public void requiredMethod2(RequiredReq req) {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("currentTransactionName:" + currentTransactionName);
//用户基本信息保存
userAccountOtherMapper.insert(req.getAccountOtherPO());
}
3.MANDATORY
如果当前有事务,则加入这个事务;如果当前没有事务,则抛出异常。表示该方法必须在一个已存在的事务中执行,否则会抛出异常。(用的比较少)
4. REQUIRES_NEW
无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起(挂起指:暂停事务的提交
),新的事务执行完毕后(这里执行完毕指:新事务都已经主动提交
),再恢复之前的事务。(注意:如果一个A方法使用@Transactional(propagation = Propagation.REQUIRED),在A方法内部调用B方法,B方法使用@Transactional(propagation = Propagation.REQUIRES_NEW)B方法事务执行报错,如果此时A方法事务是挂起状态则该事务会回滚(回滚指:sql执行完还没有提交
),只有等B方法执行提交且不报错,A方法中的事务才提交)
当需要在一个方法中处理一些独立的业务逻辑,且希望这些业务逻辑能够独立于外部事务的提交或回滚时可以用这个事务控制
@Transactional(rollbackFor = Exception.class) //开启一个事务
@Override
public void requiredNewMethod1(RequiredReq req) {
//如果下方saveAccountRecord(userQuestionRecordPO)方法执行失败,这个事务不会提交
userAccountQuotaMapper.insertAccountQuota(req.getUserAccountQuotaPO());
for (long i = 100000L; i < 100004L; i++) {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("requiredNewMethod1:" + currentTransactionName);
UserQuestionRecordPO userQuestionRecordPO = new UserQuestionRecordPO();
userQuestionRecordPO.setId(i);
//这个方法的事务是 @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW) (只有等这个事务执行完,之前挂起的事务来才能提交)
accountRecordimpl.saveAccountRecord(userQuestionRecordPO);
}
}
5. NOT_SUPPORTED
以非事务的方式执行操作,如果当前存在事务,则将当前事务挂起。(使用较少)
6. NEVER
以非事务的方式执行操作,如果当前存在事务,则抛出异常。(使用较少)
当使用该事务控制,事务会失效,不支持事务
7. NESTED
如果当前存在事务,则在嵌套事务(这个嵌套事务不是像 required 传播机制里面加入当前事务,二者不是同一个事务对象
)中执行;如果当前没有事务,则新建一个事务(这个事务不受外层影响,即使外层代码报错了,该事务方法能正常执行就会提交此时和REQUIRED效果是一样的
)。如果外层事务提交,则嵌套事务也会提交;如果外层事务回滚,则嵌套事务也会回滚。(如果外层有事务看下面的代码执行,如果内层的事务回滚,也会导致外层的事务不会提交,除非内层事务被catch)
适用于需要独立的事务,但是又需要受到外层事务的控制的情况。
//如果不加这个注解 ,如果accountQuotaimpl.saveAccountQuota(req.getUserAccountQuotaPO());能正常执行,那么会事务会独立提交
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void nestedMethod1(RequiredReq req) {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("currentTransactionName:" + currentTransactionName);
userAccountOtherMapper.insert(req.getAccountOtherPO());
//int i = 1/0;
//这个方法使用了NESTED
accountQuotaimpl.saveAccountQuota(req.getUserAccountQuotaPO());
}
//被上面方法调用
@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
@Override
public void saveAccountQuota(UserAccountQuotaPO userAccountQuotaPO) {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("currentTransactionNameQUTA:" + currentTransactionName);
userAccountQuotaMapper.insertAccountQuota(userAccountQuotaPO);
//这个如果报错,外层事务也不会提交
int j = 1/0;
}
//看下事务名称:方法执行后看两个方法的事务名称一样,但是二者不是同一个事务,和REQUIRED事务很相似
//currentTransactionNameQUTA:com.spring.review.tranctional.nested.service.impl.TransationNestedimpl.nestedMethod1
//currentTransactionName:com.spring.review.tranctional.nested.service.impl.TransationNestedimpl.nestedMethod1
验证下两个方法的事务名称一样,但是二者不是同一个事务
1.这样外层事务可以被正常提交,内存事务回滚
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void nestedMethod1(RequiredReq req) {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("currentTransactionName:" + currentTransactionName);
userAccountOtherMapper.insert(req.getAccountOtherPO());
try {
//方法里面的异常被捕获 (这里的事务使用 NESTED )
accountQuotaimpl.saveAccountQuota(req.getUserAccountQuotaPO());
} catch (Exception e) {
e.printStackTrace();
// throw new RuntimeException(e);
}
}
2.如果执行到accountQuotaimpl.saveAccountQuota(req.getUserAccountQuotaPO()) 方法报错了,而方法saveAccountQuota(req.getUserAccountQuotaPO())内又没有异常捕获,这个事务整体会被回滚,虽然最外层使用了异常捕获,但是这个方法还是会执行报错
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
@Override
public void nestedMethod1(RequiredReq req) {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("currentTransactionName:" + currentTransactionName);
userAccountOtherMapper.insert(req.getAccountOtherPO());
//int i = 1/0;
try {
//方法里面的异常被捕获 (这里的事务使用 REQUIRED )
accountQuotaimpl.saveAccountQuota(req.getUserAccountQuotaPO());
} catch (Exception e) {
e.printStackTrace();
//不抛出异常
// throw new RuntimeException(e);
}
}
//使用REQUIRED
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
@Override
public void saveAccountQuota(UserAccountQuotaPO userAccountQuotaPO) {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("currentTransactionNameQUTA:" + currentTransactionName);
userAccountQuotaMapper.insertAccountQuota(userAccountQuotaPO);
//模拟报错
int j = 1/0;
}
//最终该方法单元测试执行失败 java.lang.ArithmeticException: / by zero
//2024-05-09 19:21:35.689 [main] DEBUG o.s.j.d.DataSourceTransactionManager - Global transaction is marked as rollback-only but transactional code requested commit
总结1:REQUIRED和 NESTED 的区别:在REQUIRED 情况下,调用方存在事务的时候,被调用方使用了REQUIRED 事务传播,二者共用一个事务,如果被调用方抛异常,方法内没有被捕获,被调用方捕获catch异常,这个事务整体还是会回滚,方法会执行失败,如果是NESTED 则,被调用方作为嵌套事务,只会回滚自己的事务,不影响外层事务提交
四、编程式事务(TransactionTemplate)
在Spring中,编程式事务是通过TransactionTemplate类来实现的,开发者可以在代码中显式地调用TransactionTemplate的方法来控制事务的开始、提交和回滚。通过编程式事务,开发者可以更加灵活地控制事务的行为,但也需要更多的代码来实现事务管理逻辑。
transactionTemplate.execute(transactionStatus -> { }); 有返回结果的执行
@Resource
private TransactionTemplate transactionTemplate;
public String excuteMethod1(RequiredReq req) {
return transactionTemplate.execute(transactionStatus -> {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("excuteMethod1:" + currentTransactionName);
try {
accountQuotaimpl.saveAccountQuota(req.getUserAccountQuotaPO());
int i = 1/0;
} catch (Exception e) {
//显示回滚
// transactionStatus.setRollbackOnly();
//如果抛出异常没有设置显式回滚,try 里面的事务也会主动回滚
throw new RuntimeException(e);
}
return "";
});
}
无返回结果的执行 transactionTemplate.executeWithoutResult(transactionStatus -> { });
//需要在类中注入可使用
@Resource
private TransactionTemplate transactionTemplate;
@Override
public void excuteMethod2( RequiredReq req) {
transactionTemplate.executeWithoutResult(transactionStatus -> {
String currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName();
System.out.println("excuteMethod1:" + currentTransactionName);
try {
accountQuotaimpl.saveAccountQuota(req.getUserAccountQuotaPO());
int i = 1/0;
} catch (Exception e) {
//显示回滚
transactionStatus.setRollbackOnly();
//如果抛出异常,try 里面的事务也会主动回滚
throw new RuntimeException(e);
}
});
}
上面代码中 transactionStatus(可随意命名) 是 Spring 框架中事务管理的一部分,特别是在使用 TransactionTemplate 时。在 TransactionTemplate.execute 方法中,transactionStatus 对象提供了关于当前事务的状态信息以及一些控制事务的方法。
状态检查:
方法名称 | 解释 |
---|---|
isNewTransaction() | 检查当前是否存在一个新的事务。 |
hasSavepoint() | 检查当前事务是否有保存点(savepoint),通常用于嵌套事务。 |
isCompleted(): | 检查事务是否已经完成。 |
事务控制
方法名称 | 解释 |
---|---|
setRollbackOnly() | 标记当前事务,使其在结束时回滚,即使没有抛出异常。 |
isRollbackOnly() | 检查是否设置了回滚标记。 |
flush() | 刷新底层会话中的数据到数据库,确保所有挂起的变化被提交。 |
保存点操作(如果支持嵌套事务):
方法名称 | 解释 |
---|---|
createSavepoint() | 创建一个新的保存点。 |
releaseSavepoint(savepoint) | 释放之前创建的保存点。 |
rollbackToSavepoint(savepoint) | 回滚到指定的保存点。 |
什么是保存点(主要使编程更加灵活)
1,保存点是事务的一个状态快照,可以在事务执行期间创建。如果后续的操作失败,你可以选择回滚到这个保存点,而不是整个事务。在复杂的业务逻辑中,可能需要在一个事务内执行多个步骤。如果某个步骤失败,你可能只想回滚到失败步骤之前的状态,而不是整个事务。
可以看下下面的代码流程
transactionTemplate.execute(status -> {
// 创建保存点
Object savepoint = status.createSavepoint();
try {
// 执行第一个数据库操作
// ...等等
// 执行第二个数据库操作
// 如果这里失败了,我们只想回滚到第一个操作之后的状态
// ...
} catch (SomeException e) {
// 回滚到保存点
status.rollbackToSavepoint(savepoint);
// 可以选择处理异常或者重新尝试操作
} finally {
// 释放保存点,无论事务是否提交或回滚
status.releaseSavepoint(savepoint);
}
// 如果没有异常,这里事务会自动提交
return null;
});
总结
编程式事务管理
1.定义:编程式事务管理要求开发者通过编写代码显式地控制事务的边界(开始、提交、回滚)。
2.实现方式:通常使用 TransactionTemplate 或直接使用 PlatformTransactionManager 来管理事务。
3.代码侵入性:高度侵入性,业务逻辑代码中需要包含事务管理的代码。
4.灵活性:提供细粒度的事务控制,可以精确控制事务的边界和操作。
5.管理性:需要开发者手动管理事务,容易出错,特别是在复杂的事务逻辑中。
6.配置性:事务管理逻辑通常在代码中配置,而不是通过配置文件或注解。
7.使用场景:适用于事务逻辑复杂,需要细粒度控制的事务管理场景。
声明式事务管理
1.定义:声明式事务管理通过配置和注解来声明事务的边界和规则,不需要在业务代码中显式地编写事务管理的逻辑。
2.实现方式:基于 Spring 的 AOP 技术,通过代理对象来拦截方法调用,并在方法执行前后添加事务管理逻辑。
3.代码侵入性:无侵入性,业务逻辑代码中不需要包含事务管理的代码。
4.灵活性:相对较粗粒度的事务控制,通常是基于方法级别的。
5.管理性:管理性较强,通过声明式配置,易于理解和维护。
6.配置性:通常在配置文件中或通过注解(如 @Transactional)来配置事务规则(springboot无需配置默认开启,会自动扫描该注解)。
7.使用场景:适用于大部分的事务管理场景,特别是那些事务规则简单、统一的事务管理。
二者比较差异
1.侵入性:编程式事务管理侵入业务代码,而声明式事务管理不侵入。
2.复杂性:编程式事务管理更复杂,容易出错,但提供了更高的灵活性;声明式事务管理简单,易于维护,但灵活性较低。
3.维护性:声明式事务管理由于其配置性和非侵入性,通常更易于维护。
4.适用场景:根据事务管理的复杂度和需求选择合适的类型,简单统一的事务规则适合使用声明式事务管理,而复杂且需要细粒度控制的事务适合使用编程式事务管理。