1. 事务的回顾
在学习MySQL的过程中,我们逐渐了解到事务是由一组操作组成的集合。事务将相关操作视为一个不可分割的整体,这意味着这些操作要么全部成功提交到数据库,要么全部被撤销,从而避免出现部分成功、部分失败的情况,保障了数据的一致性和完整性。
一个完整的事务操作流程包括三个关键步骤:首先,开启事务,标志着事务处理的开始;接着,执行具体的事务操作,这些操作涉及数据的增、删、改、查等实际业务处理;最后,根据执行结果,决定是提交事务还是回滚事务。回滚事务在这里起着至关重要的“补救”作用。当程序在执行事务操作过程中遇到错误,例如数据违反约束条件、SQL语法错误或系统故障时,及时执行回滚事务可以将数据库状态恢复到事务开始之前,避免因错误操作导致的数据不一致或损坏。
2. 事务的实现方式
2.1. 编程式事务
Spring 手动操作事务和 MySQL 操作事务类似,也是分为开启事务,提交事务,回滚事务等三个操作,需要用到 DataSourceTransactionManager (事务管理器)来进行上述事务的操作,还需要用到 TransactionDefinition(事务的属性,获取事务时需要把这个类的对象传进去)
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/registy")
public String registy(String name, String password) {
//开启事务,获取一个状态,之后回滚就回滚到了这个状态
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
Integer reuslt = userService.insert(name, password);
//提交事务(提交的是之前获取的状态)
dataSourceTransactionManager.commit(transaction);
return "注册成功";
}
}
测试之后数据也是正常更新了
回滚的话调用的是 rollback 方法,再次进行插入数据,数据就没有更新,不过自增 id 还是变成了 3,对比提交事务的日志可以看出,这次没有提交事务的信息了
2.2. 声明式事务
上面的方式是比较麻烦的,需要自己写一大堆信息,来看声明式事务是如何操作的
首先需要添加依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
只需在要执行的方法上添加 @Transactional
注解,添加之后,如果没有发生异常就正常执行,如果发生了异常就回滚事务
来看异常的情况:
这时事务就没有提交,进行了回滚
3. @Transactional
@Transactional
可以用来修饰方法或类,修饰方法时,只有修饰 public 方法时才生效,修饰其他方法时不会报错,但也不生效,修饰类时,对该类中所有的 public 方法都生效
在目标方法执行开始之前会自动开启事务,执行结束之后会自动提交事务,如果方法执行过程中出现异常且异常未被捕获,就进行事务回滚操作
例如,把上面的异常代码 catch 起来,事务就正常提交了
但是如果捕获之后又进行抛出,那么事务还是会回滚的
还可以通过调用 setRollbackOnly 方法进行手动回滚
这样的话把异常捕获之后还可以回滚事务
3.1. rollbackFor
@Transactional
默认只在遇到 RuntimeException 和 Error 时才进行回滚,非运行时异常就不会滚,来演示一下发生非运行异常时的情况:
虽然此时抛出了异常,但是事务还是提交了,并没有进行回滚,可以通过设置 @Transactional
注解的 rollbackFor 属性来指定那些异常要回滚
把 rollbackFor 设置为 Exception.class,表示 Exception 底下的子类异常都会发生回滚
@Transactional(rollbackFor = Exception.class)
@RequestMapping("/r3")
public String r3(String name, String password) throws IOException {
Integer reuslt = userService.insert(name, password);
if (true) {
throw new IOException();
}
return "注册成功";
}
此时再次测试,事务就回滚了
3.2. isolation
@Transactional
注解的 isolation 属性是可以设置事务的隔离级别的,参数类型是一个 Isolation 的枚举类,依次表示当前数据库默认使用的隔离级别和事务的四种隔离级别
可以根据需要进行设置
//设置事务的隔离级别
@Transactional(isolation = Isolation.DEFAULT)
@RequestMapping("/r4")
public String r4(String name, String password) throws IOException {
Integer reuslt = userService.insert(name, password);
if (true) {
throw new IOException();
}
return "注册成功";
}
4. 事务传播机制
事务传播机制是指在多个事务方法相互调用时,定义事务如何在这些方法之间传播的规则,也就是延用调用方法的事务还是再重新开启一个新事务
Spring 事务的传播机制有以下七种
事务传播机制 | 详细描述 | 案例理解(设有 A、B 两个方法,A 调用 B 时,针对 B 而言) |
---|---|---|
1. Propagation.REQUIRED | 这是事务传播的默认级别。在执行操作时,若当前已经存在一个事务,那么该操作会加入到这个现有的事务中,与事务内的其他操作共同遵循事务的一致性原则。若当前没有事务正在进行,系统会自动创建一个新的事务,确保操作在事务的管理下进行,以此保证数据的完整性和一致性。 | 假设方法 A 开启了一个事务,当 A 调用方法 B 时,方法 B 会直接使用 A 所开启的事务,与 A 中的操作一起提交或回滚。若方法 A 没有开启事务,那么在调用 B 时,方法 B 会自行开启一个新的事务来执行其内部操作。 |
2. Propagation.SUPPORTS | 此传播机制下,如果当前存在事务,操作会加入到该事务中,遵循事务的规则。但如果当前没有事务,操作将以非事务的方式继续执行,即不会受到事务的约束,数据的提交或回滚不会按照事务的机制来处理。 | 当方法 A 开启了事务并调用方法 B 时,方法 B 会加入到 A 的事务中。而若方法 A 没有开启事务,那么方法 B 会按照非事务的方式执行,其操作的结果会立即生效,不会受到事务的统一管理。 |
3. Propagation.MANDATORY | 该传播机制要求当前必须存在一个事务。当进行操作时,如果当前有事务,操作会加入到这个事务中。但如果当前没有事务,系统会抛出异常,提示操作无法在无事务的环境下进行,以此确保操作必须在事务的管控下执行。 | 若方法 A 开启了事务,当 A 调用方法 B 时,方法 B 会加入到 A 的事务里。但要是方法 A 没有开启事务,那么在调用 B 时,系统会抛出异常,阻止方法 B 以非事务的方式执行。 |
4. Propagation.REQUIRES_NEW | 无论当前是否存在事务,被此传播机制修饰的方法都会开启一个属于自己的新事务。若当前存在事务,会将现有的事务挂起,新事务独立于原事务运行,两个事务的提交、回滚等操作互不影响,各自维护自己的数据一致性。 | 不管方法 A 是否开启了事务,当 A 调用方法 B 时,方法 B 都会开启一个全新的事务。若 A 本身有事务,会先将 A 的事务挂起,B 的事务独立执行,其提交或回滚操作不会影响 A 的事务,反之亦然。 |
5. Propagation.NOT_SUPPORTED | 这种传播机制规定操作以非事务的方式运行。若当前存在事务,会将该事务挂起,使操作不受事务的约束,数据的更改会立即生效,而不会等待事务的统一提交或回滚。 | 不管方法 A 是否开启了事务,当 A 调用方法 B 时,方法 B 都会以非事务的方式执行。若 A 有事务,会将 A 的事务挂起,B 中的操作结果会直接写入数据库,不会参与 A 事务的提交或回滚流程。 |
6. Propagation.NEVER | 此传播机制要求操作必须以非事务的方式运行。若当前存在事务,系统会抛出异常,以保证操作不会在事务环境下执行,确保数据的更改不遵循事务的规则。 | 方法 B 会以非事务的方式执行。当方法 A 调用方法 B 时,如果方法 A 开启了事务,系统会抛出异常,阻止 B 在事务环境下执行,保证 B 的操作不受事务的影响。 |
7. Propagation.NESTED | 当当前存在事务时,会创建一个嵌套事务。嵌套事务是当前事务的子事务,它有自己独立的保存点,可以独立回滚,但整体上依赖于外层事务。若外层事务回滚,嵌套事务也会回滚。若当前没有事务,该传播机制的行为等同于 PROPAGATION_REQUIRED,即会创建一个新的事务。 | 若方法 A 开启了事务,当 A 调用方法 B 时,方法 B 会创建一个嵌套在 A 事务中的子事务。方法 B 可以有自己独立的回滚操作,但如果方法 A 的事务回滚,方法 B 的嵌套事务也会随之回滚。若方法 A 没有开启事务,方法 B 会像 PROPAGATION_REQUIRED 一样,自行开启一个新的事务。 |
4.1. REQUIRED
把 UserService 和 LogService 的两个方法都设置为 REQUIRED
@Slf4j
@RestController
@RequestMapping("/propaga")
public class PropagationController {
@Autowired
private UserService userService;
@Autowired
private LogService logService;
@Transactional
@RequestMapping("/r1")
public String registy(String name, String password) {
userService.insert(name, password);
logService.insertLog(name,"用户注册");
return "注册成功";
}
}
在 PropagationController 中进行调用,此时 registy 就相当于 A ,调用的两个方法相当于 B,运行之后,如果其中一个方法发生异常,那么 registy 方法的整个事务都会回滚,也就是他们都用的是 A 的事务
4.2. REQUIRES_NEW
把 UserService 和 LogService 的两个方法都设置为 REQUIRES_NEW
此时就是无论 A 有没有事务, B 都新创建事务,所以当 B 的一个方法有异常时,是不会影响其他方法的
4.3. NEVER
如果设置为 NEVER 的话,A 调用 B,A 如果存在事务,就会报错
把 A 的事务取消掉就不会报错了
4.4. NESTED
NESTED 是如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,所以说 A 和 B 不是同一个事务,那么当 B 的一个方法出现异常时进行回滚,另一个 A 调用的方法是不受影响的,也印证了这两个不是同一个事务,确实是创建了一个嵌套事务
和 REQUIRED 不同的是,那里用的是同一个事务,其中一个回滚,都要回滚,这里可以只是自己的事务进行回滚,也就是实现局部回滚
文章来源: https://study.disign.me/article/202508/6.spring-transaction-propagation.md
发布时间: 2025-02-20
作者: 技术书栈编辑