什么是事务?
我们在开发企业应用时,通常业务人员的一个操作实际上是对数据库读写的多步操作的结合。由于数据库在顺序执行的过程中,任何一步都有可能发生异常,异常会导致后续操作无法完成,此时由于业务逻辑并未正确的完成,之前的操作的数据并不可靠,如果要让这个业务正确的执行下去,通常有实现的方式:
1.记录失败的位置,问题修复之后,从上一次执行失败的位置开始继续执行后面要做的业务逻辑。
2.在执行失败的时候,回退本次执行的所有过程,让操作恢复到原始状态,待问题修复之后,重新执行原来的业务逻辑
事务就是针对上诉方式2的实现。事务,一般是指要做的或所做的事情,就是上面所说业务人员的一个操作(比如电商系统中,一个创建订单的操作包含了创建订单、商品库存的扣减两个基本的操作。如果创建订单成功,库存扣减失败,那么就会出现商品超卖的问题,所以最基本的就是需要为这两个操作用事务包括起来,保证这两个操作要么都成功,要么都失败)。
未开启事务
无事务运行,保留出错前的数据,不涉及回滚。
@Test
public void contextLoads(){
int money2 = 100;
//jerry
Useru useru3 = userDao.selectUserById(1);
//tom
Useru useru4 = userDao.selectUserById(2);
//jerry - money
userService.updUser(
new Useru(useru3.getId(),useru3.getName(),useru3.getMoney()-money2));
System.out.println(useru3.getName() + " - "+ money2);
/*制造异常*/
int result = 1/0;
//tom + money
userService.updUser(
new Useru(useru4.getId(),useru4.getName(),useru4.getMoney()+ money2));
System.out.println(useru4.getName() + " - "+ money2);
}
开启事务
在头部加上
@Transactional
1. 无嵌套的正常事务
public class C{
@Transactional
public void a(){
//TODO
}
}
【结论】:出现异常,正常回滚。
原因:
事务的 ACID 特性中的 C,Consistency 一致性,事务内有操作失败时则数据将全部回滚到修改前的状态。
2. 方法A调用带事务的方法B
public class C{
public void a(){
b();
}
@Transactional
public void b(){
//TODO
}
}
【结论】:同一个类内,方法 A 调用了带事务的方法 B,则方法 B 的事务将失效。
原因:
spring aop 动态代理机制给当前类 C 生成代理类 PolicyC,然后 A 调用 B 还是在 C 类中调用(而不是代理类 PolicyC),代理类 PolicyC 中的 B 方法并没有被调用到。
解决方案:
只有在不同的类 C 和 C1 中,C 类中的 A 方法调用 C1 类中的 B 方法,这样 B 中的事务就会生效。
public class C{ @Autowired private C1 c1; //注入 C1 类调用 c1 类中的 B() 方法 public void A(){ c1.B(); } } public class C1{ @Transactional public void B(){ //TODO } }
3. 加入异常捕获处理的事务
public class C{
@Transactional
public void a(){
try {
//TODO
} catch (Exception e) {
e.printStackTrace();
}
}
}
【结论】:事务方法中对原子操作进行普通的 try-catch 时,事务会失效。
原因:
默认spring事务只在发生未被捕获的 RuntimeExcetpion 时才回滚。
Spring 声明式事务默认会对非检查型异常和运行时异常进行回滚,而
对检查型异常不进行回滚
操作。Error和RuntimeException及其子类为非检查型异常。通过如下几种方式改变其规则:1)让 checked 异常也进行回滚,使用 @Transactional(rollbackFor = Exception.class)
2)让 unchecked 异常也不回滚,使用 @Transactional(notRollbackFor = RunTimeException.class)
3)不需要事务,@Transactional(propagation = Propagation.NOT_SUPPORTED)
4)如果不添加 rollbackFor 等属性,Spring碰到 Unchecked Exceptions 都会回滚,不仅是RuntimeException,也包括 Error。
因此,本场景下会让 spring aop 无法正常捕捉到运行时的事务中的异常,从而事务失效。
解决方案:
- 在 catch 语句中,继续抛出 Exception。不建议这么做,不太友好(调用者需捕获处理异常)。
public class C{ @Transactional public void a() throws Exception { try { //TODO } catch (Exception e) { e.printStackTrace(); throw new Exception(); //或用 throw new RuntimeException() 则无需往上继续抛,可被 aop 捕获进而事务回滚 } } }
对于要进行事务的方法,不使用 try catch,在上层调用的时候处理异常;
说明:同样不能再同一个类内,原因参考 方法A调用带事务的方法B 场景。在 catch 语句中增加 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();语句,手动回滚,这样上层就无需去处理异常。
public class C{ @Transactional public void a() { try { //TODO } catch (Exception e) { e.printStackTrace(); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); //手动回滚 } } }
4. 事务回滚的注意事项
- 要想事务起作用,必须是主方法名上有 @Transactional 注解,方法体内不能用 try catch;如果用 try catch,则 catch 中必须用 throw new RuntimeException();
- @Transactional 注解应该只被应用到 public 方法上,不要用在 protected、private 等方法上,即使用了也将被忽略,不起作用,这是由 Spring AOP 决定的。
- 只有来自外部的方法调用才会被 AOP 代理捕捉,类内部方法调用类内部的其他方法,子方法并会不引起事务行为,即使被调用的方法上使用有 @Transactional 注解。
- 类内部方法调用内部的其他方法,被调用的方法体中如果有 try catch,则 catch 中必须用 throw new RuntimeException(),否则即使主方法上加上 @Transactional 注解,如果被调用的子方法出错也不会抛出异常,不会引起事务起作用。
5. 事务传播行为分析
所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务的执行行为。
七大事务传播行为:
传播行为 | 含义 |
---|---|
Propagation.REQUIRED | 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个事务执行(默认的事务传播行为) |
Propagation.SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行 |
Propagation.MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 |
Propagation.REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起 |
Propagation.NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起 |
Propagation.NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常(与 MANDATORY 相反) |
Propagation.NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,可独立与外围事务进行单独提交或者回滚。;如果当前没有事务,则该取值等价于 REQUIRED |
5.0 无事务
无事务运行,保留出错前的数据,不涉及回滚。
5.1 REQUIRED 示例解析
没有事务则创建一个事务执行(默认的事务传播行为)。
@Transactional 等价于 @Transactional(propagation = Propagation.REQUIRED)
5.2 REQUIRES_NEW 示例解析
场景①:新起的事务抛出异常会不会让外围事务回滚?会!
场景②:外围事务失败会不会导致新起的事务中已提交的数据进行回滚?会!
5.3 SUPPORTS 示例解析
场景①:调用者不存在事务,则不会回滚。
场景②:调用者存在事务,则会回滚。
5.4 NOT_SUPPORTED 示例解析
当前不支持事务。比如 ServiceA.methodA 的事务级别是 PROPAGATION_REQUIRED ,而 ServiceB.methodB 的事务级别是 PROPAGATION_NOT_SUPPORTED ,那么当执行到 ServiceB.methodB 时,ServiceA.methodA 的事务挂起,而他以非事务的状态运行完,再继续 ServiceA.methodA 的事务。
如果是同一个类内,该事务级别不会生效。
即 A 类的 a 方法使用事务 REQUIRED,调用 A 类内部的 b 方法使用事务 NOT_SUPPORTED 时,不论 a或b 谁出现异常,则都会回滚。(已验证)
5.5 MANDATORY 示例解析
必须在事务中运行,否则就会抛出异常。
5.6 NEVER 示例解析
不能在事务中使用,否则抛出异常。而且在非事务中使用时,一旦出现异常也不会进行回滚。
5.7 NESTED 示例解析
嵌套事务,外围如果没有事务则自己另起一个事务,可独立与外围事务进行自身事务的单独提交或者回滚
。