Spring实现事务的两种方式

464 阅读6分钟

前言:本篇文章我将从以下几个方面由浅入深的和大家一起探讨我们在spring项目中如何使用事务:

  • spring实现事务的方式

  • 项目中应该选择哪种方式

  • 通过源码结合图文分析spring实现事务的逻辑

一、Spring实现事务的两种方式

  1. 声明式事务
    • 使用@Transactional注解定义声明式事务
  2. 编程式事务
    • 使用AspectJ的AOP配置编程式事务
    • 使用Spring提供的TransactionTemplate实现编程式事务
    • 直接使用JDBC的API来实现编程式事务

1.1 @Transactional应用

1. 注解位置:

- 加在类上:本类的所有public方法都添加事务
- 加在方法上:为方法添加事务,会覆盖加在类上的`@Transactional`

2. 入门案例

测试没有异常

@Service
public class AnnotateDemoService {
    @Resource
    private UserMapper userMapper;

    @Transactional
    public boolean insertOne(User user) {
        return userMapper.insertOne(user);
    }
}
  • 查看测试结果

image.png

有异常测试

@Service
public class AnnotateDemoService {
    @Resource
    private UserMapper userMapper;

    @Transactional
    public boolean insertOne(User user) {
        userMapper.insertOne(user);
        //除零错误
        System.out.println(10/0);
    }
}
  • 查看结果:没有添加成功,说明事务起作用了。

image.png

3.Transactional参数

参数含义
isolation事务隔离级别,默认为DEFAULT
propagation事务传播机制,默认为REQUIRED
readOnly事务读写性,默认为false
noRollbackFor一组异常类,遇到该组类时不回滚,默认为{}
noRollbackForClassName一组异常类名,遇到时该组类时不回滚,默认为{}
rollbackFor一组异常类,遇到时回滚,默认为{}
rollbackForClassName一组异常类名,遇到时回滚,默认为{}
timeout超时时间,以秒为单位
value可选的限定描述符,指定使用的事务管理器,默认为“”

3.1 rollbackFor参数应用举例

自定义异常类

public class MyException extends Exception{
}

编写service层方法

@Transactional
public void updateUsername(User user) throws MyException {
     userMapper.updateUsername(user);
     throw new MyException();
}

编写单元测试方法

@Test
void updateTest01() throws MyException {
    User user = new User();
    user.setUsername("update name");
    user.setId(1);
    annotateDemoService.updateUsername(user);
}

结果如图:

image.png 我们添加上rollbackfor属性再试试

@Transactional(rollbackFor = MyException.class)
public void updateUsername(User user) throws MyException {
     userMapper.updateUsername(user);
     throw new MyException();
}

结果如图:

image.png 总结:Spring事务默认处理运行时异常(RuntimeException)和错误(Error),对于其他异常并不回滚。因此提供了rollbackFor属性来指定要回滚的异常。

4. spring事务隔离级别及传播行为

  1. 事务隔离级别
隔离级别含义
DEFAULTSpring 中默认的事务隔离级别,以连接的数据库的事务隔离级别为准。Mysql:可重复读;Oracle:读已提交
READ_UNCOMMITTED读未提交
READ_COMMITTED读已提交
REPEATABLE_READ可重复读
SERIALIZABLE串行化
  1. 事务传播行为

事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。

如:方法A定义的事物传播行为是PROPAGATION_SUPPORTS,那么当方法B调用方法A时,如果B有事务,则A加入B的事务,如果B没有事务,则A以非事务形式执行。

spring中定义了7种事务传播行为

传播行为含义
PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。Spring默认的传播行为
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
  1. 传播行为应用举例
/*
    insertOne方法调用updateSex方法,updateSex会采用默认的传播行为(PROPAGATION_REQUIRED)
    即在本例中,updateSex会加入insertOne的事务中。如果insetOne抛异常了,那么updateSex同样会回滚
 */
@Transactional
public void insertOne(User user) {
    /*注意:
       1. 这里不能插入一个用户再改变这个用户的性别,因为插入事务还没有提交,这行记录被锁住了,更新性别的事务无法执行。
       2. updateSex和insertOne不能再同一个类中,不然会导致@Transactional失效。原因请看后文
     */
    userMapper.insertOne(user);
    annotateDemoService2.updateSex(user);
    throw new RuntimeException();
}

@Service
public class AnnotateDemoService2 {
    @Resource
    private UserMapper userMapper;

    @Transactional
    public void updateSex(User user) {
         //改变另一个用户的性别。
        user.setId(1);
        user.setSex(true);
        userMapper.updateSex(user);
    }
}

单元测试

@Test
void updateTest02(){

    User user = new User();
    user.setUsername("test2");
    user.setSex(true);
    annotateDemoService.insertOne(user);
}

运行结果如图:

image.png

数据库的记录如图:没有test2的用户,用户id为1的性别也没有更新。说明抛异常后事务回滚了

image.png 现在我们指定传播行为再试试

*
 现在指定了传播行为是REQUIRES_NEW,意味着是两个不同是事务。那么insertOne抛异常将不会影响到updateSex
 */
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateSex(User user) {
    userMapper.updateSex(user);
}

运行结果如图:

image.png 正常的运行日志如图:

image.png 通过日志我们知道REQUIRES_NEW传播行为会先把内层的事务提交,再提交外层事务。其他的传播行为感兴趣的同学可以自行尝试验证。

5. @Transactional失效场景

  • 标注在非public方法上
  • @Transactional 注解属性 propagation 设置错误
    • SUPPORTSNOT_SUPPORTEDNEVER如果某方法的事务定义的是这三种传播行为之一,则有可能发生事务不回滚现象。
  • 抛出的异常不是RuntimeException或其子类,也不是Error
    • 对于这种异常要通过rollbackFor属性指定异常类
  • 同一个类中方法调用,导致@Transactional失效
    • 对于同一类中的方法自我调用,Spring无法通过代理对象进行代理,因为代理对象在创建过程中是通过AOP切面AspectJ实现的,会生成代理类覆盖目标类中的所有方法,而对于自我调用,是直接调用目标类内部定义的方法,没有经由代理类转发,所以@Transactional注解不生效。而对于其他bean调用,因为是通过代理对象调用目标类的方法,所以@Transactional注解可以生效。
    • 为了使自我调用也能被代理,在 Spring 中,可以使用AspectJ代理模式(CGLib)来创建代理对象。 AspectJ代理方式可以为同一类中的方法自我调用生成代理对象,从而保证事务注解生效。要使用AspectJ代理模式,需要将代理方式设置为CGLib,例如在 @EnableTransactionManagement 注解中加入 mode = AdviceMode.ASPECTJ
  • 数据库引擎不支持事务
    • 例如MyISMA 引擎是不支持事务操作的

参考文章: