Spring事务详解

一、事务基本概念

1.1 事务的四大特性

  • 事务的四大特性(ACID): 原子性、一致性、持久性、隔离性
  • 原子性(Atomicity):一个事务的操作,要么全部完成,要么全部不完成
  • 一致性(Consistency):事务开始之前和事务结束之后,数据库的完整性没有被破坏
  • 持久性(Isolation):事务提交后,对数据的修改就是永久的
  • 隔离性(Durability):数据库允许多个并发事务同时对其数据读写和修改的能力,隔离性可以防止多个事务并发执行时导致的数据不一致

1.2 事务的隔离级别

事务的隔离级别: 在数据库中,多个事务并发执行时,各个事务之间是相互隔离的,每个事务之间都不能相互干扰。为了保证并发事务的正确性和一致性,数据库管理系统(DBMS)提供了四种事务隔离级别,分别为读未提交、读已提交、可重复读和串行化。

  • 读未提交(Read Uncommitted)
    在该隔离级别下,一个事务可以读取另一个事务尚未提交的数据,不仅如此,它可能还会读到一些脏数据(Dirty Read)。在该级别下,数据的一致性和完整性得不到保障,不推荐使用。

  • 读已提交(Read Committed)
    在该隔离级别下,一个事务只能读取另一个事务已经提交的数据,避免了脏读的问题,但是可能会出现不可重复读(Non-Repeatable Read)的问题。即同一事务中,多次读取同一数据可能会得到不同的结果,因为其他事务可能在两次读取之间提交了修改。

  • 可重复读(Repeatable Read)
    在该隔离级别下,一个事务在执行期间多次读取同一数据时,能够保证所读取的数据一定是事务开始时的状态,避免了不可重复读的问题,但是可能会出现幻读(Phantom Read)问题,即两次查询间新增数据的影响。

  • 串行化(Serializable)
    在该隔离级别下,所有事务串行执行,避免了脏读、不可重复读和幻读的问题,但是实际应用中可能会导致性能严重下降,因此该级别一般只在特殊情况下使用。

OREACLE 的默认的事务隔离级别为读已提交,mysql 的默认的事务隔离级别为 可重复读。
默认隔离级别为REPEATABLE READ

在这里插入图片描述

  • 脏读:读取到别的事务未提交的数据;
  • 不可重复读:同一事务多次读取,读取到的记录内容不一致
  • 幻读:同一事务多次读取到的数据集条数记录不同。强调幻读在于某一个范围内的数据行变多或者是变少了,侧重说明的是数据集不一样导致了产生了幻读

幻读是说数据的条数发生了变化,原本不存在的数据存在了。不可重复读是说数据的内容发生了变化,原本存在的数据的内容发生了改变。

1.3 spring 七个事务的传播机制

Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。Propagation枚举则引用了这些类型,开发过程中我们一般直接用Propagation枚举。 所谓事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。

1.3.1 Propagation源码

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
 
    private final int value;
 
    private Propagation(int value) {
        this.value = value;
    }
 
    public int value() {
        return this.value;
    }
}

Spring对事务控制的支持统一在TransactionDefinition类中描述,该类有以下 几个重要的接口方法:

  • int getPropagationBehavior():事务的传播行为
  • int getIsolationLevel():事务的隔离级别
  • int getTimeout():事务的过期时间
  • boolean isReadOnly():事务的读写特性

除了事务的传播行为外,事务的其他特性Spring是借助底层资源的功能来完成的,Spring无非只充当个代理的角色。但是事务的传播行为却是Spring凭借自身的框架提供的功能 ;

1.3.2、类型解析

  • Propagation.REQUIRED(默认机制):如果当前没有事务,就新建一个事务,如果已经存在一个事务,就加入到这个事务中。这是最常见的选择。
  • Propagation.SUPPORTS(支持事务):支持当前事务,如果当前没有事务,就以非事务方式执行。
  • Propagation.MANDATORY(强制使用事务):使用当前的事务,如果当前没有事务,就抛出异常
  • Propagation.REQUIRES_NEW(新建事务):新建事务,如果当前存在事务,把当前事务挂起。
  • Propagation.NOT_SUPPORTED(不支持事务):以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • Propagation.NEVER(非事务):以非事务方式执行,如果当前存在事务,则抛出异常。
  • Propagation.NESTED(事务嵌套):如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。。

Spring默认的事务传播行为是PROPAGATION_REQUIRED,它适合绝大多数的情况,如果多个ServiveX#methodX()均工作在事务环境下(即均被Spring事务增强),且程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这3个服务类的3个方法通过Spring的事务传播机制都工作在同一个事务中。

二、spring 中事务的实现

2.1.spring 中事务的使用

Spring中的事务分为两类:
1.编程式事务(手动操作)
2.声明式事务(自动提交事务)

2.1.1 编程式事务

SpringBoot内置了两个对象,DataSourceTransactionManager用来获取事务(开启事务、提交事务、回滚事务)。TransactionDefinition是事务的属性,在获取事务时,需要将TransactionDefinition传递进去获取一个TransactionStatus

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;
    @Autowired
    private DataSourceTransactionManager transactionManager;
    @Autowired
    private TransactionDefinition transactionDefinition;

    @RequestMapping("/del")
    public int del(Integer id) {
        if(id != null && id > 0) {
            //开启事务
            TransactionStatus transactionStatus =
                    transactionManager.getTransaction(transactionDefinition);
            //删除用户业务操作
            int result = userService.del(id);
            System.out.println("删除了: " + result);
            // 提交事务/回滚事务
//            transactionManager.commit(transactionStatus); //提交事务
            transactionManager.rollback(transactionStatus); //回滚事务
        }
        return 0;
    }
}


2.1.2 声明式事务

声明式事务我们只需要在方法上@Transactional注解就可以实现,无需手动进行开启事务和提交事务,进入方法自动开启,执行完毕自动提交,发生异常后会自动回滚事务

@RestController
@RequestMapping("/user2")
public class UserController2 {

    @Autowired
    private UserService userService;

    @Transactional
    @RequestMapping("/del")
    public int del(Integer id) {
        if(id == null || id <= 0) {
            return 0;
        }
        return userService.del(id);
    }
}

这种是没有异常的异常,我们会进行commit,我们目前数据库数据

2.1.3 @Transactional的使用

@Transactional的作用范围:@Transactional既可以用来修饰方法也可以用来修饰类
1.修饰方法:只能应用到public方法上,否则不生效,推荐用法
2.修饰类:表明该注解对所有的public方法都生效

参数作用
我们可以通过设置@Transactional的一些参数来决定事务的一些具体的功能
在这里插入图片描述

2.1.4 Spring 五个事务的隔离级别

Spring的事务隔离级别是有以下5种的
在这里插入图片描述
1.DEFAULT:使用底层数据库默认的隔离级别。
2.READ_UNCOMMITTED:允许读取还未提交的数据,会出现脏读、不可重复读和幻读等问题。
3.READ_COMMITTED:只能读取已经提交的数据,避免了脏读的问题,但是可能出现不可重复读和幻读的问题。
4.REPEATABLE_READ:保证同一事务中多次读取同一记录结果是一致的,避免了脏读和不可重复读的问题,但是仍然可能出现幻读的问题。
5.SERIALIZABLE:最高的隔离级别,保证事务串行执行,避免了脏读、不可重复读和幻读等问题,但是影响系统性能。

默认情况下,Spring 会使用底层数据库的默认隔离级别,通常是READ_COMMITTED 级别。可以通过事务管理器的 setDefaultTransactionIsolation() 方法或在事务注解中使用 isolation 属性来设置隔离级别,例如:

@Transactional(isolation = Isolation.DEFAULT)
    @RequestMapping("/del")
    public int del(Integer id) {
    }

2.2.spring 中事务的实现原理

  1. spring容器在启动的时候会判断是否含有@Transactional 注解,如果有则生基于该类bean的子类代理beanProxy。
  2. 在业务层调用该@Transactional 注解代理的方法,则被SpringAOP拦截处理,通过SpringAOP处理。
  • 2.1 首先spring会调用代理对象,对于事务,代理对象会通过执行事务的切面逻辑,开启事务和数据库连接,设置conn.autocommit 设置为 false。(spring默认是开启了自动提交,当SQL执行结束之后就会提交,当遇到异常的时候,由于前面的事务都已经提升,因此就没法回滚了,所以需要把自动提交给关闭了) 。
  • 2.2 子类代理beanProxy调用原来bean 执行业务代码 SQL。最后在通过第一次创建的对象(Spring个生命周期中通过构造方法创造的对象)去执行test方法。接着会去执行SQL语句,在此SQL执行完之后是不会进行提交的,在执行SQL语句之前,jdbcTemplate会去拿到事务管理器创建的这个数据库连接conn。
  • 2.3 当执行完test方法后,Spring事务会去判断是否有异常,没有异常就会提交事务(conn.commit()),否者就会事务回滚(conn.rollback());

在这里插入图片描述

2.3 spring中事务失效的场景

2.3.1 被非public 修饰符修饰

java的访问权限有4种:private、default、protected、public,它们的权限从左到右,以此变大。如果在开发中,将事务方法定义了错误的访问权限,则事务功能会失效。

@Service
public class EmpService {

    @Transactional
    private void add(UserModel userModel){
        saveData(userModel);
    }
}

如上:add方法的权限被定义成了private,这样会导致事务失效,spring要求被代理方法必须是public的。、

在Spring源码中,如果目标方法不是public,则TransactionAttribute返回null,不支持事务。

2.3.2 方法用final修饰

@Service
public class EmpService {

    @Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
    }
}

放方法被final修饰时,也会导致事务失效。

因为spring事务底层是用了aop,用了jdk的动态代理或者cglb的动态代理,会帮我们生成代理类,在代理类中实现事务功能。

如果某个方法被final修饰了,那么在代理类中,就无法重新该方法,而添加事务功能。

注意:如果某个方法被static修饰,同样也无法通过动态代理,变成事务方法。

2.3.3 直接调用内部方法

add 方法的事务生效,updateSataus 方法的事务不会生效

@Service
public class EmpService {

    @Transactional
    public void add(UserModel userModel){
        saveData(userModel);
        updateSataus(userModel);
    }


    @Transactional
    public void updateSataus(UserModel userModel){
        doSomething();
    }
}

由上可知:在事务方法add可知,它直接调用了updateStatus方法,由2可知:方法拥有事务的能力是因为spring aop中生成了代理对象,但是直接调用updateStatus方法不会直接生成事务。但是可以直接将该类直接注入进来,比如:

@Service
public class EmpService {

    private EmpService empService;

    @Transactional
    public void add(UserModel userModel){
        saveData(userModel);
        empService.updateSataus(userModel);
    }


    @Transactional(rollbackFor = Exception.class)
    public void updateSataus(UserModel userModel){
        doSomething();
    }
}

这样updateSataus方法事务就生效了,add方法的事务也会生效,也不会穿生循环依赖的问题。

2.3.4 未被Spring管理

public class EmpService {


    @Transactional
    public void add(UserModel userModel){
        saveData(userModel);
        updateSataus(userModel);
    }
    
}

如图:没有将该类交给spring管理,需要在配置声明类中
@EnableTransactionManagement // 开启事务注解,等同于配置文件tx:annotation-driven/

2.3.5 多线程调用

由以下代码可知:在add()事务方法里面,调用了updateStatus事务方法,但是updateStatus事务方法是在另外一个线程中调用的。这样就导致了两个方法不在同一个线程中,获取到了数据库连接不一样,从而是两个不同的事务,如果updateStatus方法中抛出了异常,add方法是不会回滚的

@Service
public class EmpService {

    @Autowired
    private OrderService orderService;


    @Transactional
    public void add(UserModel userModel){

        new Thread(()->{
            orderService.updateSataus();
        }).start();
    }

}

@Service
public class OrderService{

    @Transactional
    public void updateSataus(){
        System.out.println("======================");
    }

}

spring的事务是通过数据库的连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。同一个事务,指同一个数据库连接,只有拥有同一个事务连接才能保证同时提交和回滚。如果是不同的线程,拿到的数据库连接肯定是不同的。

解决办法,捕获子线程抛出的异常,然后手动回滚

2.3.6 表不支持事务

如果表的引擎是myisam,那么它是不支持事务的,要想支持事务,改成innodb引擎

2.3.7 事务没有开启

如果是springBoot项目,那么是事务默认是开启的,但如果是spring项目,需要xml配置

2.3.8 事务的传播特性

如果事务的传播特性设置错了,事务也会失效。如下:propagation = Propagation.NEVER这种类型的传播特性不支持事务,如果有事务会抛出异常。
目前只有这三种传播特性才会创建新事物:REQUIRED、REQUIRES_NEW、NESTED

@Service
public class EmpService {


    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel){
        saveData(userModel);
        updateSataus(userModel);
    }

}

2.3.9 自己吞了异常

事务不会回滚,最常见的问题是:开发者在代码中手动try…catch了异常。如下:

@Service
public class EmpService {


    @Transactional
    public void add(UserModel userModel){
        try {
            saveData(userModel);
            updateSataus(userModel);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

这种情况下,因为开发者自己捕获了异常、又没有手动抛出

2.3.10 手动抛了别的异常

如果抛的异常不正确,事务也不会回滚

@Service
public class EmpService {


    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
            saveData(userModel);
            updateSataus(userModel);
        } catch (Exception e) {

            throw new Exception(e);
        }
    }

}

因为Spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的非运行时异常,它不会回滚。

2.4 事务回滚

2.4.1 自定义回滚异常

如果在使用@Transactional注解声明事务时,有时想自定义回滚异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。如下:

@Service
public class EmpService {


    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) {
            saveData(userModel);
            updateSataus(userModel);
    }

}

但是如果在程序执行过程中,出现了sql异常,但是sql异常并不属于我们定义的BusinessException异常,所以事务也不会回滚

@Service
public class EmpService {

    @Autowired
    private OrderService orderService;


    @Transactional
    public void add(UserModel userModel) {
        saveData(userModel);
        orderService.updateSataus();
    }

}

@Service
public class OrderService(){

    @Transactional(propagation = Propagation.NESTED)
    public void updateSataus(){
        System.out.println("======================");
    }
}

如果说对于这样的代码。如果saveData执行成功了,updataStatus执行失败了,那么整个add方法就会回滚。那么之前saveData执行成功的这个方法也会回滚。

那如何只让报错的方法进行回滚呢?只需要将报错的方法抓取异常就OK了。如下:

2.4.2 嵌套事务导致多回滚了怎么办

@Service
public class EmpService {

    @Autowired
    private OrderService orderService;


    @Transactional
    public void add(UserModel userModel) {
        saveData(userModel);
        try {
            orderService.updateSataus();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

@Service
public class OrderService(){

    @Transactional(propagation = Propagation.NESTED)
    public void updateSataus(){
        System.out.println("======================");
    }
}

面试题:A 事务调用B事务方法,如何在B事务方法报错的情况下,A事务继续执行

答:A方法的事务隔离级别为 Propagation.Required ,B方法的事务隔离级别为Propagation.NESTED。A方法手动try catch B的方法,当B的方法报错的时候,不予处理,即可;

2.4.3 手动回滚事务的方法

在Spring中,你可以使用TransactionAspectSupport.currentTransactionStatus()方法来获取当前事务的状态,并手动回滚事务。以下是一个示例代码:

import org.springframework.transaction.interceptor.TransactionAspectSupport;
 
public void someMethod() {
    try {
        // 业务逻辑代码
    } catch (Exception ex) {
        // 手动回滚事务
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

确保你的方法someMethod是在一个事务的环境中执行的,例如通过@Transactional注解。如果没有当前的事务,currentTransactionStatus()将会抛出异常。

三、事务方法和非事务方法的调用

5.1 同一个类中,普通方法调用事务方法,事务失效

在同一个类中,一个普通方法调用另外一个有注解(比如@Async,@Transational)事务的方法,注解是不会生效的,事务失效;
例子中,有两方法,一个有@Transational注解,一个没有。如果调用了有注解的addPerson()方法,会启动一个Transaction;如果调用updatePersonByPhoneNo(),因为它内部调用了有注解的addPerson(),如果你以为系统也会为它启动一个Transaction,那就错了,实际上是没有的

@Service
public class PersonServiceImpl implements PersonService {
    
    @Autowired
    PersonDao personDao;
    
    @Override
    @Transactional
    public boolean addPerson(Person person) {
        boolean result = personDao.insertPerson(person)>0 ? true : false;
        return result;
    }
    
    @Override
    //@Transactional
    public boolean updatePersonByPhoneNo(Person person) {
        boolean result = personDao.updatePersonByPhoneNo(person)>0 ? true : false;
        addPerson(person); //测试同一个类中@Transactional是否起作用
        return result;
    }
}

原因:
spring 在启动的时候会扫描bean的方法上是否包含@Transactional注解,如果包含,spring会为这个bean动态地生成一个子类(即代理类,proxy),代理类是继承原来那个bean的。此时,当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用之前就会启动transaction。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个bean(即spring本身创建的bean,没有任何代理),所以就不会启动transaction,我们看到的现象就是@Transactional注解无效

即:Spring的事务管理是通过AOP实现的,其AOP的实现对于非final类是通过cglib这种方式,即生成当前类的一个子类作为代理类,然后在调用其下的方法时,会判断这个方法有没有@Transactional注解,如果有的话,则通过动态代理实现事务管理(拦截方法调用,执行事务等切面)。当b()中调用a()时,发现b()上并没有@Transactional注解,所以整个AOP代理过程(事务管理)不会发生。

在这里插入图片描述

简化代码示例
为什么一个方法a()调用同一个类中另外一个方法b()的时候,b()不是通过代理类来调用的呢?可以看下面的例子(为了简化,用伪代码表示):

@Service
class A{
    @Transactinal
    method b(){...}

    method a(){    //标记1
    b();
    }
}

//Spring扫描注解后,创建了另外一个代理类,并为有注解的方法插入一个startTransaction()方法:

class proxy$A{
    A objectA = new A();
    
    method b(){    //标记2
        startTransaction();
        objectA.b();
        closeTransaction();
    }

    method a(){    //标记3
        objectA.a();    //由于a()没有注解,所以不会启动transaction,而是直接调用A的实例的a()方法
    }
}

当我们调用A的bean的a()方法的时候,也是被proxyA 拦截,执行 p r o x y A拦截,执行proxyA拦截,执行proxyA.a()(标记3),然而,由以上代码可知,这时候它调用的是objectA.a(),也就是由原来的bean来调用a()方法了,所以代码跑到了“标记1”。由此可见,“标记2”并没有被执行到,所以startTransaction()方法也没有运行

解决的方法:
1.把这两个方法分开到不同的类中,在启动的时候,spring为两个方法都加载不同的事务代理类;
2.在启动类上加注解 @EnableAspectJAutoProxy(exposeProxy = true)
3.把注解加到类名上面,相当于每个方法都加了事务;

结论:
在一个Service内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务.

spring采用动态代理机制来实现事务控制,而动态代理最终都是要调用原始对象的,而原始对象在去调用方法时,是不会再触发代理了!

Spring的事务管理是通过AOP实现的,其AOP的实现对于非final类是通过cglib这种方式,即生成当前类的一个子类作为代理类,然后在调用其下的方法时,会判断这个方法有没有@Transactional注解,如果有的话,则通过动态代理实现事务管理(拦截方法调用,执行事务等切面)。当b()中调用a()时,发现b()上并没有@Transactional注解,所以整个AOP代理过程(事务管理)不会发生。

5.2 同一个类中,事务方法之间的嵌套调用不会开启新的事务

事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务!

述的情况,说白了,就是在一个Service内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务
其实通过上面的动态代理的代码,你应该可以发现:
动态代理最终都是要调用原始对象的,而原始对象在去调用方法时,是不会再触发代理了!

那么如何解决呢?

很简单,我们完全可以在抽出一个XxxService,在其内部调用UserService.txMethod()和UserService.txMethod2()方法即可。总而言之,避免在一个Service内部进行事务方法的嵌套调用!(因为动态代理导致这种场景事务失效了。

四、事务传播实战

4.1 Propagation.REQUIRED

j结论:

  • 对于正常的单个方法调用,更新、保存两张以上的表的时候如果不使用事务,会破坏插入数据的一致性;
  • 对于嵌套的方法调用。第一个方法都是 Propagation.REQUIRED 事务传播机制,第二个方法没有事务,那么对于第一个方法执行成功后,在调用第二个方法报错的情况下。第一个方法的插入数据不会插入到数据库(异常回滚),第二个方法也不会插入执行成功的数据。只要有调用方法有事事务,被调用事务在没有事务Transactional修饰的情况下也会默认使用调用方法的事务

4.1.1 普通的方法事务插入数据

    public String propagationRequired(String s) {
        TGoods tGoods = new TGoods();
        tGoods.setDescription("ropagationRequired");
        tGoods.setCreateTime(new Date());
        tGoods.setName("ropagationRequired");
        tGoods.setPrice(1111);
        tgoodsDao.insert(tGoods);
        User user = new User();
        userDao.insert(null);
        return "ropagationRequired";
    }

如果不加@Transactional 注解,在第二张表插入报错的时候会造成第一张表数据插入成功,第二章表数据插入失败

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@231b2d91] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1519298501 wrapping com.mysql.cj.jdbc.ConnectionImpl@9ea0dad] will not be managed by Spring
==>  Preparing: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?) 
==> Parameters: null, null, null, null
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@231b2d91]
2024-05-24 22:51:50.547 ERROR 13284 --- [nio-9005-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet]      : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: 
### Error updating database.  Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
### The error may exist in file [F:\private\dd\zhang-micro-services\business\amaster-work\amaster-work-server\target\classes\mapper\UserDao.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: insert into user(username, mobile, create_by, create_time)         values (?, ?, ?, ?)
### Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
; Column 'username' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null] with root cause

java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.15.jar:8.0.15]
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.15.jar:8.0.15]
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.15.jar:8.0.15]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:970) ~[mysql-connector-java-8.0.15.jar:8.0.15]

4.1.2 第一个方法都是 Propagation.REQUIRED 事务传播机制,第二个方法没有事务的插入

  @Transactional
    public String propagationRequiredNestedCall (String s) {
        TGoods tGoods = new TGoods();
        tGoods.setDescription("ropagationRequired");
        tGoods.setCreateTime(new Date());
        tGoods.setName("ropagationRequired");
        tGoods.setPrice(66666);
        tgoodsDao.insert(tGoods);
        this.propagationRequiredNestedCallExcute();
        return "propagationRequiredNestedCall";
    }

    public String propagationRequiredNestedCallExcute() {
        TGoods tGoods = new TGoods();
        tGoods.setDescription("propagationRequiredNestedCallExcute");
        tGoods.setCreateTime(new Date());
        tGoods.setName("propagationRequiredNestedCallExcute");
        tGoods.setPrice(777777);
        tgoodsDao.insert(tGoods);
        User user = new User();
        userDao.insert(null);
        return "propagationRequiredNestedCallExcute";
    }

执行结果

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
JDBC Connection [HikariProxyConnection@1343699016 wrapping com.mysql.cj.jdbc.ConnectionImpl@34b3bf07] will be managed by Spring
==>  Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?) 
==> Parameters: ropagationRequired(String), 66666(Integer), ropagationRequired(String), 2024-05-25 00:15:31.847(Timestamp), null
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338] from current transaction
==>  Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?) 
==> Parameters: propagationRequiredNestedCallExcute(String), 777777(Integer), propagationRequiredNestedCallExcute(String), 2024-05-25 00:15:34.178(Timestamp), null
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338] from current transaction
==>  Preparing: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?) 
==> Parameters: null, null, null, null
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
2024-05-25 00:15:42.573 ERROR 8144 --- [nio-9005-exec-2] o.a.c.c.C.[.[.[.[dispatcherServlet]      : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: 
### Error updating database.  Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
### The error may exist in file [F:\private\dd\zhang-micro-services\business\amaster-work\amaster-work-server\target\classes\mapper\UserDao.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: insert into user(username, mobile, create_by, create_time)         values (?, ?, ?, ?)
### Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
; Column 'username' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null] with root cause

java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.15.jar:8.0.15]
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.15.jar:8.0.15]

数据库插入结果,可以看到没有插入数据
在这里插入图片描述

4.2 Propagation.SUPPORTS

结论:如果调用 Propagation.SUPPORTS 的方法有事务,那么Propagation.SUPPORTS 的方法就支持事务;如果调用 Propagation.SUPPORTS的方法没有事务,那么Propagation.SUPPORTS 的方法就不支持事务

4.2.1 没有事务的情况下

  @Transactional(propagation =Propagation.SUPPORTS)
    public String supportsNotTransactional(String s) {
        TGoods tGoods = new TGoods();
        tGoods.setDescription("propagationsupports");
        tGoods.setCreateTime(new Date());
        tGoods.setName("propagationsupports");
        tGoods.setPrice(2222);
        tgoodsDao.insert(tGoods);
        User user = new User();
        userDao.insert(null);
        return "test/supports";

    }

执行日志

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250]
JDBC Connection [HikariProxyConnection@1936043538 wrapping com.mysql.cj.jdbc.ConnectionImpl@6342fd64] will be managed by Spring
==>  Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?) 
==> Parameters: propagationsupports(String), 2222(Integer), propagationsupports(String), 2024-05-24 23:08:29.455(Timestamp), null
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250] from current transaction
==>  Preparing: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?) 
==> Parameters: null, null, null, null
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250]
2024-05-24 23:08:29.960 ERROR 14380 --- [nio-9005-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet]      : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: 
### Error updating database.  Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
### The error may exist in file [F:\private\dd\zhang-micro-services\business\amaster-work\amaster-work-server\target\classes\mapper\UserDao.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: insert into user(username, mobile, create_by, create_time)         values (?, ?, ?, ?)
### Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
; Column 'username' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null] with root cause

java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.15.jar:8.0.15]
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.15.jar:8.0.15]
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.15.jar:8.0.15]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:970) ~[mysql-connector-java-8.0.15.jar:8.0.15]

插入结果
在这里插入图片描述

可以看到ropagation.SUPPORTS 直接使用的时候是没有事务的,所以在插入的时候没有支持事务,在User 插入报错的时候,造成TGoods 插入数据库成功了。

4.2.1 有事务的情况下

 @Transactional
    public String supportsHaveTransactional(String s) {
        TGoods tGoods = new TGoods();
        tGoods.setDescription("propagationsupportsHave");
        tGoods.setCreateTime(new Date());
        tGoods.setName("propagationsupportsHave");
        tGoods.setPrice(3333);
        tgoodsDao.insert(tGoods);
        this.supportsHaveTransactionalExcute();
        return "test/supports";
    }

    @Transactional(propagation =Propagation.SUPPORTS)
    public void supportsHaveTransactionalExcute() {
        TGoods tGoods = new TGoods();
        tGoods.setDescription("supportsHaveTransactional---Excute");
        tGoods.setCreateTime(new Date());
        tGoods.setName("supportsHaveTransactional--Excute");
        tGoods.setPrice(44444);
        tgoodsDao.insert(tGoods);
        User user = new User();
        userDao.insert(null);
    }

可以看到supportsHaveTransactional 有默认的事务,supportsHaveTransactionalExcute 为支持事务。当supportsHaveTransactional调用 supportsHaveTransactionalExcute方法的时候有事务,所以supportsHaveTransactionalExcute 执行的时候也是有事务的,所以supportsHaveTransactionalExcute 中插入第二张表的时候tGoods 值为 4444 的这条记录不会插入到数据库中。由于this.supportsHaveTransactionalExcute 插入的时候抛出异常所以supportsHaveTransactional方法在插入tGoods 为333的记录也不会插入成功;

执行日志

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
JDBC Connection [HikariProxyConnection@803423368 wrapping com.mysql.cj.jdbc.ConnectionImpl@55aa0209] will be managed by Spring
==>  Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?) 
==> Parameters: propagationsupportsHave(String), 3333(Integer), propagationsupportsHave(String), 2024-05-24 23:35:49.214(Timestamp), null
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6] from current transaction
==>  Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?) 
==> Parameters: supportsHaveTransactional--Excute(String), 44444(Integer), supportsHaveTransactional---Excute(String), 2024-05-24 23:35:55.049(Timestamp), null
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6] from current transaction
==>  Preparing: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?) 
==> Parameters: null, null, null, null
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
2024-05-24 23:35:59.796 ERROR 3280 --- [nio-9005-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet]      : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: 
### Error updating database.  Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
### The error may exist in file [F:\private\dd\zhang-micro-services\business\amaster-work\amaster-work-server\target\classes\mapper\UserDao.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: insert into user(username, mobile, create_by, create_time)         values (?, ?, ?, ?)
### Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
; Column 'username' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null] with root cause

java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.15.jar:8.0.15]
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.15.jar:8.0.15]
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.15.jar:8.0.15]
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:970) ~[mysql-connector-java-8.0.15.jar:8.0.15]

数据库执行结果
可以看到数据库supportsHaveTransactional 方法的插入和supportsHaveTransactionalExcute 方法在日志中插入成功的结果也没有插入到数据库中,印证了上面的猜想
在这里插入图片描述

4.3 Propagation.MANDATORY(强制使用事务)

结论:调用Propagation.MANDATORY 的方法必须要有事务,不然会报错

4.3.1

 @Transactional(propagation=Propagation.MANDATORY)
    public String mandatoryTransactional(String s) {
        TGoods tGoods = new TGoods();
        tGoods.setDescription("mandatoryTransactional");
        tGoods.setCreateTime(new Date());
        tGoods.setName("mandatoryTransactional");
        tGoods.setPrice(5555);
        tgoodsDao.insert(tGoods);
        User user = new User();
        user.setCreateTime(new Date());
        user.setUsername("lisi");
        user.setMobile("21312321321");
        userDao.insert(user);
        return "mandatoryTransactional";
    }

日志

2024-05-24 23:51:35.112 ERROR 19140 --- [nio-9005-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet]      : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'] with root cause

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:362) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:574) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:361) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at com.zhang.buiness.amaster.workserver.service.WorkTransactionalServiceImpl$$EnhancerBySpringCGLIB$$1dc41d04.mandatoryTransactional(<generated>) ~[classes/:na]
	at com.zhang.buiness.amaster.workserver.service.WorkTransactionalServiceImpl$$FastClassBySpringCGLIB$$4c0f1875.invoke(<generated>) ~[classes/:na]
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.2.8.RELEASE.jar:5.2.8.RELEASE]

数据库也是没有插入记录的

4.3.2 @Transactional(propagation=Propagation.MANDATORY) 方法调用没有@Transactional事务的方法

可以看到没有@Transactional事务的方法 插入的使用也是默认使用事务的

  @Transactional
    public String mandatoryTransactionalNestedCall(String s) {
        this.mandatoryTransactionalNestedCallB();
        return "mandatoryTransactionalNestedCall";
    }
    @Transactional(propagation=Propagation.MANDATORY)
    public void mandatoryTransactionalNestedCallB() {
        this.mandatoryTransactionalNestedCallC();
    }

    public void mandatoryTransactionalNestedCallC() {
        TGoods tGoods = new TGoods();
        tGoods.setDescription("mandatoryTransactionalNestedCallC");
        tGoods.setCreateTime(new Date());
        tGoods.setName("mandatoryTransactionalNestedCallC");
        tGoods.setPrice(888);
        tgoodsDao.insert(tGoods);
        User user = new User();
        userDao.insert(null);
    }

执行日志

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99]
JDBC Connection [HikariProxyConnection@1704205209 wrapping com.mysql.cj.jdbc.ConnectionImpl@18b1f281] will be managed by Spring
==>  Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?) 
==> Parameters: mandatoryTransactionalNestedCallC(String), 888(Integer), mandatoryTransactionalNestedCallC(String), 2024-05-25 00:30:32.542(Timestamp), null
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99] from current transaction
==>  Preparing: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?) 
==> Parameters: null, null, null, null
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99]
2024-05-25 00:30:37.471 ERROR 24288 --- [nio-9005-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet]      : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: 
### Error updating database.  Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
### The error may exist in file [F:\private\dd\zhang-micro-services\business\amaster-work\amaster-work-server\target\classes\mapper\UserDao.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: insert into user(username, mobile, create_by, create_time)         values (?, ?, ?, ?)
### Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
; Column 'username' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null] with root cause

java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.15.jar:8.0.15]
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.15.jar:8.0.15]

执行结果 可以看到没有插入结果
在这里插入图片描述

  • 0
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值