前言

事务的传播性是Spring特有的概念,是基于Spring AOP技术实现的,原本的方法不具备事务的功能,运用Spring AOP的方式动态的增加了事务的功能,来确保数据库的数据的一致性。

只要开启事务的方法发生调用关系就一定存在事务的传播,重点在于调用才有传播,调用就存在调用者和被调用者,事务传播就是研究调用者和被调用者之间的关系。

7种传播机制的约束条件

约束条件

说明

REQUIRED

如果当前没有事务,则新建事务,如果当前存在事务,则加入当前事务,合并成一个事务

REQUIRES_NEW

新建事务,如果当前存在事务,则把当前事务挂起,新建事务执行完后再恢复当前事务

NESTED

如果当前没有事务,则新建事务,如果当前存在事务,则创建一个当前事务的子事务(嵌套事务),子事务不能单独提交,只能和父事务一起提交

SUPPORTS

支持当前事务,如果当前没有事务,以非事务的方式执行

NOT_SUPPORTED

以非事务方式执行,如果存在当前事务就把当前事务挂起

NEVER

以非事务方式执行,如果当前存在事务就抛异常

MANDATORY

使用当前事务,如果当前没有事务,就抛异常

上述的约束条件只有 REQUIRED 最常用,也是默认的约束条件, REQUIRES_NEW 可能会被用到,剩下的 SUPPORTS,NOT_SUPPORTED,NEVER,NESTED,MANDATORY 都是不常用的,我以 REQUIRED 做研究,其它不考虑。

首先先搞懂传播机制的时机,其实就是被调用者被调用者调用的时候

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void transfer(String inName, String outName, float money) {
    yangMapper.outMoney(outName, money);
    yangMapper.inMoney(inName, money);
}

问:从上代码可以看出采用REQUIRED传播,不加propagation参数默认也是REQUIRED(@Transactional),我们知道这种传播模式有新建和加入事务两种,请问能否判断出transfer方法是新建事务还是加入事务?
答:不能,因为该方法还没有发生调用,只有发生调用的时候注解才生效,所以无法判断。

再看下面的代码

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public int transfer(String inName, String outName, float money) {
    yangMapper.outMoney(outName, money);
    yangMapper.inMoney(inName, money);
    return 1;
}

//伪代码
void main(){
	// 开启事务
	transfer("yang","c",100f);
	// 提交事务
}

问:请问能否判断出transfer方法是新建事务还是加入事务?
答:能,因为该方法还发生调用,调用者是main方法,被调用者是transfer,并且是新建事务,因为main方法没有事务,被调用者只能新建事务,相反如果main方法有事务,那么被调用者只需要加入事务。

// 被调用者加入事务样例
// @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
// void main(){
//	  开启事务
//    transfer("yang","c",100f);
//	  提交事务
// }

现在展示下事务的使用场景,我要演示的场景分两种,一个是A类的A方法调用B类的B方法,另一个是A类的A方法调用A类的B方法

这个是Controller层的方法的入口,调用A类的A方法

@RequestMapping("/yang")
public Integer addMoney() {
    serviceImplA.addMoney("yang", 100);
    return 1;
}

A类的A方法调用B类的B方法

A类的A方法

// A类的A方法
public void addMoney(String yang, int i) {
    Map<String, Object> map = new HashMap<>();
    map.put("name", yang);
    map.put("money", i);
    yangMapper.addMoney(map); // 调用加钱方法 100
    serviceImplB.addMoney(yang, i);// 调用B类的B方法
}

B类的B方法

// B类的B方法
public void addMoney(String yang, int i) {
    Map<String, Object> map = new HashMap<>();	
    map.put("name", yang);
    map.put("money", i);
    yangMapper.addMoney(map);// 调用加钱方法 200
}

mapper和provider中的方法,方法意思就是给yang加100元

// mapper
@UpdateProvider(type = YangProvider.class, method = "addMoney")
void addMoney(Map<String, Object> map);
// provider
public String addMoney(Map<String, Object> map) {
    StringBuffer sb = new StringBuffer();
    sb.append("UPDATE YANGMONEY SET MONEY = MONEY + " + map.get("money") + " WHERE NAME = '" + map.get("name") + "'");
    return sb.toString();
}

经过上述的操作调用结果为给yang增加200元,这个是毫无争议的对吧

java 事物传播特性 java事务传播机制_数据库


如果现在B类的B方法下加个运行异常语句,如下

// B类的B方法
public void addMoney(String yang, int i) {
    Map<String, Object> map = new HashMap<>();	
    map.put("name", yang);
    map.put("money", i);
    yangMapper.addMoney(map);// 调用加钱方法 200
    int e = 1 / 0;
}

那么现在如果重新执行,数据库的结果会是什么呢,是的,虽然会抛出异常但是数据库已经操作完了,结果是200,这种就是非常不合理的,明明程序有问题还给我修改了数据库数据,现在我们就给B类的B方法加上事务,再看结果会是多少,能猜到吗?

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void addMoney(String yang, int i) {
    Map<String, Object> map = new HashMap<>();
    map.put("name", yang);
    map.put("money", i);
    yangMapper.addMoney(map);// 调用加钱方法 200
    int e = 1 / 0;
}

最终的结果是100,因为B类的B方法加上了事务就相当于A类的A方法在调用B类B方法的时候就变成了

public void addMoney(String yang, int i) {
    Map<String, Object> map = new HashMap<>();
    map.put("name", yang);
    map.put("money", i);
    yangMapper.addMoney(map); // 调用加钱方法 100
    // 开启事务
    serviceImplB.addMoney(yang, i);// 调用B类的B方法
    // 提交事务
}

可以看到B类的B方法被调用的时候是有事务的,B类方法的B方法是有异常的,B方法是一个整体,遇到异常就回滚了,所以数据库最后的只入了A类A方法加钱,B类的回滚了,为100,如果不是在B类B方法开启的事务,而是在A类的A方法上开始的事务,那么结果又会是什么样子呢

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void addMoney(String yang, int i) {
    Map<String, Object> map = new HashMap<>();
    map.put("name", yang);
    map.put("money", i);
    yangMapper.addMoney(map); // 调用加钱方法
    serviceImplB.addMoney(yang, i);// 调用B类的B方法
}

这样最后的结果是数据库不会增加钱,结果为0,因为你在A类的A方法开启事务,A类的A方法和调用B类的B方法又是一个整体了,B有异常就一起回滚了,看在哪里开启的事务是要看调用者的,在A类A方法上加事务,开启事务的时机是

@RequestMapping("/yang")
public Integer addMoney() {
    // 开启事务
    serviceImplA.addMoney("yang", 100);
    // 提交事务
    return 1;
}

就算把那个异常语句移动到A类的A方法中,效果都是一样的,因为都在事务包裹中,是一个整体。如下

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
   public void addMoney(String yang, int i) {
       Map<String, Object> map = new HashMap<>();
       map.put("name", yang);
       map.put("money", i);
       yangMapper.addMoney(map); // 调用加钱方法
       serviceImplB.addMoney(yang, i);// 调用B类的B方法
       int e = 1 / 0;
   }

A类的A方法调用A类的B方法

A类的A方法调用A类的B方法,A类的B方法和A类的A方法一个方法,方便看结果,如下

public void addMoney(String yang, int i) {
       Map<String, Object> map = new HashMap<>();
       map.put("name", yang);
       map.put("money", i);
       yangMapper.addMoney(map); // 调用加钱方法
       addMoney1(yang, i);
   }

   public void addMoney1(String yang, int i) {
       Map<String, Object> map = new HashMap<>();
       map.put("name", yang);
       map.put("money", i);
       yangMapper.addMoney(map);// 调用加钱方法
   }

这样数据库的结果会是200这个是肯定的对吧,现在我们就和上面一样,在addMoney1里面抛异常,如下

public void addMoney1(String yang, int i) {
      Map<String, Object> map = new HashMap<>();
      map.put("name", yang);
      map.put("money", i);
      yangMapper.addMoney(map);// 调用加钱方法
      int e = 1 /0;
  }

结果是一样的,数据库的结果肯定还是200,现在给addMoney1加事务,如下

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void addMoney1(String yang, int i) {
    Map<String, Object> map = new HashMap<>();
    map.put("name", yang);
    map.put("money", i);
    yangMapper.addMoney(map);// 调用加钱方法
    int e = 1 / 0;
}

你们觉得数据库的数据会是多少,我想肯定会有人觉得数据库的数据是100,但是错误了,数据是200
很明显出现这种结果就是事务失效了,这样加事务是加不上的,事务的底层是Spring AOP来实现的,这种自调用的方式是不满足AOP的动态代理的,如果你想要让这个事务生效,你在A类A方法中调用的时候不能采用 addMoney1(yang, i) 的方式,应该用A类的对象调用才行,如下

public void addMoney(String yang, int i) {
    Map<String, Object> map = new HashMap<>();
    map.put("name", yang);
    map.put("money", i);
    yangMapper.addMoney(map); // 调用加钱方法
    // 开启事务
    serviceImplA.addMoney1(yang, i);// 修改自调用
    // 提交事务
}

或者也可以在A类A方法上加事务,如下,这样是没问题的addMoney1是一个整体不是开启事务的时机

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void addMoney(String yang, int i) {
    Map<String, Object> map = new HashMap<>();
    map.put("name", yang);
    map.put("money", i);
    yangMapper.addMoney(map); // 调用加钱方法
    addMoney1(yang, i);
}

@Transactional的几种失效场景

  • 访问权限问题(只有 public 方法会生效)。
  • 方法用 final 修饰,不会生效。

spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。如果某个方法是static的,同样无法通过动态代理,变成事务方法。

  • 同一个类中的方法直接内部调用,会导致事务失效。
  • 类本身未被spring管理。

在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。

  • 多线程调用。
@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。

  • 存储引擎不支持事务。
  • 自己吞了异常。

spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出runtime异常。如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。尽量不要写 try-catch 如果要写的同时还要保证事务回滚可以尝试在catch最后一行throw一个runtimeException或者手动回滚。

声明式事务与编程式事务

① 声明式事务

通常情况下,我们会在方法上@Transactional注解,填加事务功能,比如:

@Service
public class UserService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
       query1();
       query2();
       query3();
       roleService.save(userModel);
       update(userModel);
    }
}


@Service
public class RoleService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void save(UserModel userModel) throws Exception {
       query4();
       query5();
       query6();
       saveData(userModel);
    }
}

但@Transactional注解,如果被加到方法上,有个缺点就是整个方法都包含在事务当中了。

上面的这个例子中,在UserService类中,其实只有这两行才需要事务:

roleService.save(userModel);
update(userModel);

在RoleService类中,只有这一行需要事务:

saveData(userModel);

现在的这种写法,会导致所有的query方法也被包含在同一个事务当中。如果query方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。

② 编程式事务

  1. @Transactional注解是通过Spring的AOP起作用的,但是如果使用不当,事务功能可能会失效。
  2. @Transactional注解一般加在某个业务方法上,会导致整个业务方法都在这个事务中,粒度太大,不好控制事务范围。

上面的这些内容都是基于@Transactional注解的,主要讲的是它的事务问题,我们把这种事务叫做:声明式事务。

其实,spring还提供了另外一种创建事务的方式,即通过手动编写代码实现的事务,我们把这种事务叫做:编程式事务。例如:

@Autowired
   private TransactionTemplate transactionTemplate;
   
   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute(transactionStatus -> {
            addData1();
            updateData2();
            return Boolean.TRUE;
        });
   }

在spring中为了支持编程式事务,专门提供了一个类:TransactionTemplate,在它的execute方法中,就实现了事务的功能。

相较于@Transactional注解声明式事务,更建议大家使用,基于TransactionTemplate的编程式事务。主要原因如下:

  1. 避免由于spring aop问题,导致事务失效的问题。
  2. 能够更小粒度的控制事务的范围,更直观。

建议在项目中少使用@Transactional注解开启事务。但并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。

事务中避免远程调用

我们在接口中调用其他系统的接口是不能避免的,由于网络不稳定,这种远程调的响应时间可能比较长,如果远程调用的代码放在某个事物中,这个事物就可能是大事务。当然,远程调用不仅仅是指调用接口,还有包括:发MQ消息,或者连接redis、mongodb保存数据等。