likes
comments
collection
share

Spring 中的事务 和 事务的传播机制

作者站长头像
站长
· 阅读数 18

1. 事务是什么?为什么需要事务?

  事务是一种用于管理数据库操作的机制。它将一组操作封装成一个单元,确保数据库操作要么全部成功提交,要么全部回滚,以保持数据的一致性和完整性。

  为什么要使用事务呢?请看下面的案例。

  假设有两个用户的银行账户,账户A和账户B,它们分别存储着一定的金额。现在,用户A想要向用户B转账100元。这个转账操作需要以下两个步骤:

  1. 从用户A的账户中扣除100元。
  2. 将扣除的100元添加到用户B的账户中。

  在这个过程中,我们需要确保两个步骤要么同时成功提交,要么同时回滚。如果第一步执行成功了,第二步却执行失败了,那么B没有收到这100块钱,A的钱就不翼而飞了。所以如果其中一个步骤出现问题,我们必须回滚整个事务,以保持数据的一致性。

2. Spring 中事务的实现

2.1 编程式事务

  Spring Boot中内置了两个对象,即:DataSourceTransactionManagerTransactionDefinition,用这两个对象就可以来操作事务了。

  这里已经配置了相应的数据库环境

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

    @Autowired
    private UserService userService;

    //DataSourceTransactionManager: 数据源事务管理器
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

	//TransactionDefinition:事务定义    
    @Autowired
    private TransactionDefinition transactionDefinition;

    //根据 id 删除数据
    @RequestMapping("/delete")
    public Integer delete(Integer id){
        //开启事务
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

        //对数据库操作:删除
        Integer result = userService.delete(1);

        //提交事务 or 回滚事务
        //回滚事务
        dataSourceTransactionManager.rollback(transactionStatus);
        //提交事务
        //dataSourceTransactionManager.commit(transactionStatus);
        return result;
    }
  • 使用DataSourceTransactionManagergetTransaction方法开始一个事务。
  • getTransaction方法中传递一个TransactionDefinition对象来定义事务的属性。
  • getTransaction方法返回一个TransactionStatus对象,表示当前事务的状态。
  • 在事务执行过程中,可以通过TransactionStatus对象来检查事务的状态。
  • 最终,通过调用dataSourceTransactionManagercommitrollback方法提交或回滚事务。

  下面是更为完整、规范的代码:

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

    @Autowired
    private UserService userService;

    @Autowired
    private DataSourceTransactionManager transactionManager;

    @Autowired
    private TransactionDefinition transactionDefinition;

    @RequestMapping("/delete")
    public Integer delete(Integer id){
        if(id == null || id <= 0){
            return 0;
        }

        TransactionStatus transactionStatus = null;
        int result = 0;
        try{
            //开启事务
            transactionStatus = transactionManager.getTransaction(transactionDefinition);
            //业务操作,删除事务
            result = userService.delete(id);
            System.out.println("删除:" + result);
            //提交事务
            transactionManager.commit(transactionStatus);
        }catch (Exception e){
            //回滚事务
            if(transactionStatus != null){
                transactionManager.rollback(transactionStatus);
            }
        }
        return result;
    }
}

  但是这种方式太繁琐了,还有更为简单的方法👇。

2.2 声明式事务(注解)

  使用 @Transactional 注解:

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

    @Autowired
    private UserService userService;

    @RequestMapping("/delete2")
    @Transactional
    public Integer delete2(Integer id){
        if(id == null || id <= 0){
            return 0;
        }
        int result = userService.delete(id);
        System.out.println("删除:" + result);
        return result;
    }
}

  无需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务

  待删除数据的表,这里删除“张三”:

Spring 中的事务 和 事务的传播机制

  进行访问后:

Spring 中的事务 和 事务的传播机制

Spring 中的事务 和 事务的传播机制

说明事务提交成功。

2.2.1 发生异常的时候

(1)没有处理的异常会自动回滚事务

  下面的代码会抛出异常,这时候再看看事务是否会回滚。

@RequestMapping("/delete2")
@Transactional
public Integer delete2(Integer id){
    if(id == null || id <= 0){
        return 0;
    }
    int result = userService.delete(id);
    int x = 8 / 0; //会抛出 ArithmeticException 异常
    System.out.println("删除:" + result);
    return result;
}

Spring 中的事务 和 事务的传播机制

Spring 中的事务 和 事务的传播机制

中途发生了没有处理的异常会自动回滚事务

(2)处理后的异常不会自动回滚事务

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/delete2")
    @Transactional
    public Integer delete2(Integer id){
        if(id == null || id <= 0){
            return 0;
        }
        int result = 0;
        try {
            //删除数据
            result = userService.delete(id);
            System.out.println("删除:" + result);
            int x = 8 / 0;
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
        return result;
    }
}

  访问前的表:

Spring 中的事务 和 事务的传播机制

  删除 id=4

Spring 中的事务 和 事务的传播机制

  访问后的表:

Spring 中的事务 和 事务的传播机制

  可以看到处理了异常后,事务没有回滚,这样的操作非常的危险,但是也有解决的方法,那就是手动回滚事务:

@RequestMapping("/delete2")
@Transactional
public Integer delete2(Integer id){
    if(id == null || id <= 0){
        return 0;
    }
    int result = 0;
    try {
        result = userService.delete(id);
        System.out.println("删除:" + result);
        int x = 8 / 0;
    }catch (Exception e){
        System.out.println(e.getMessage());
        //手动回滚事务
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
    return result;
}

  这样它就会回滚事务了。

2.2.2 @Transactional 作用范围

  @Transactional 可以加在方法上以及类上,但是:

  • 当使用 @Transactional 注解修饰方法时,它只对public的方法生效。
  • 当使用 @Transactional 注解修饰类时,表示对该类中所有的public方法生效。

  这是因为基于代理的事务管理机制在运行时创建代理对象,并且代理对象只能访问public方法。当@Transactional注解应用于非public方法(如protectedprivate或默认包可见性的方法)时,代理对象无法访问这些方法,导致事务管理无法生效。

2.2.3 @Transactional 参数说明

下面是补充的 @Transactional 注解的参数及其作用的汇总表格:

参数描述
value 指定事务管理器的名称。
propagation指定事务的传播行为。
isolation 指定事务的隔离级别。
readOnly 指定事务是否为只读。
timeout 指定事务的超时时间(以秒为单位)。
rollbackFor 指定哪些异常触发事务回滚。
rollbackForClassName指定哪些异常类名触发事务回滚。
noRollbackFor指定哪些异常不触发事务回滚。
noRollbackForClassName指定哪些异常类名不触发事务回滚。
  1. propagation:指定事务的传播行为(后文详细介绍)。
  2. isolation:指定事务的隔离级别,定义了事务之间的可见性和并发控制(后文详细介绍)。
  3. readOnly:指定事务是否为只读,如果设置为 true,则表示该事务只读取数据,不修改数据。
  4. timeout:指定事务的超时时间,单位为秒。如果事务执行时间超过指定的超时时间,则事务会被强制回滚。
  5. rollbackFor:指定哪些异常触发事务回滚。可以指定一个或多个异常类型的数组。
  6. rollbackForClassName:指定哪些异常类名触发事务回滚。可以指定一个或多个异常类名的字符串数组。
  7. noRollbackFor:指定哪些异常不触发事务回滚。可以指定一个或多个异常类型的数组。
  8. noRollbackForClassName:指定哪些异常类名不触发事务回滚。可以指定一个或多个异常类名的字符串数组。

2.2.3 @Transactional 工作原理

  Spring 通过代理模式实现 @Transactional 的工作原理。当一个带有 @Transactional 注解的方法被调用时,Spring 将创建一个代理对象来管理事务。Spring 使用 AOP(面向切面编程)将事务管理逻辑织入到带有 @Transactional 注解的方法周围。这样,在方法执行前后,会插入事务管理相关的代码。

下面是方法调用的详细过程:

  1. 调用者通过代理对象调用被代理的方法。
  2. 代理对象接收到方法调用请求。
  3. 代理对象在方法调用前执行预定义的逻辑,例如事务管理的开始。
  4. 代理对象将实际的方法调用委托给原对象。这意味着代理对象将真正的方法调用传递给原对象,使原对象执行实际的业务逻辑。
  5. 原对象执行方法的实际逻辑。
  6. 原对象返回方法的结果给代理对象。
  7. 代理对象在方法调用后执行额外的逻辑,例如事务管理的提交或回滚。
  8. 代理对象将方法的结果返回给调用者。

Spring 中的事务 和 事务的传播机制

3. 事务的隔离级别

3.1 事务特性

事务具有以下四个重要的特性,通常被称为 ACID 特性:

  1. 原子性(Atomicity):原子性要求事务被视为不可分割的最小工作单元,要么全部执行成功,要么全部失败回滚。事务在执行过程中发生错误或中断,系统必须能够将其恢复到事务开始前的状态,保证数据的一致性。
  2. 一致性(Consistency):一致性确保事务在执行前后数据库的状态是一致的。事务在执行过程中对数据库进行的修改必须满足预定义的规则和约束,以保证数据的完整性。
  3. 隔离性(Isolation):隔离性指多个事务并发执行时,每个事务的操作都应当与其他事务相互隔离,使它们感觉不到其他事务的存在。隔离性可以防止并发执行的事务之间发生干扰和数据冲突,确保数据的正确性。
  4. 持久性(Durability):持久性要求事务一旦提交,其对数据库的修改就是永久性的,即使在系统发生故障或重启的情况下,修改的数据也能够被恢复。持久性通过将事务的结果写入非易失性存储介质(如磁盘)来实现。

3.2 事务的隔离级别

对于隔离性,通常有以下四个标准的隔离级别:

  1. Read Uncommitted(读取未提交数据):最低的隔离级别。在该级别下,一个事务可以读取到另一个事务未提交的数据,可能导致脏读,即读取到了未经验证的数据。这个级别会导致数据的不一致性,并且不提供任何并发控制。
  2. Read Committed(读取已提交数据):在该级别下,一个事务只能读取到已经提交的数据。它避免了脏读,但可能出现不可重复读(Non-repeatable Read)的问题。不可重复读是指同一个事务中多次读取同一数据,在事务执行过程中,该数据被其他事务修改,导致每次读取到的值不一致。
  3. Repeatable Read(可重复读):在该级别下,一个事务在执行期间多次读取同一数据时,保证能够读取到一致的结果。即使其他事务对该数据进行修改,也不会影响当前事务的读取操作。这个级别通过锁定读取的数据,避免了不可重复读,但可能出现幻读(Phantom Read)的问题。幻读是指同一个事务中多次查询同一个范围的数据时,由于其他事务插入了新的数据,导致每次查询结果集不一致。
  4. Serializable(可串行化):最高的隔离级别,它要求事务串行执行,完全避免了并发问题。在该级别下,事务之间互相看不到对方的操作,可以避免脏读、不可重复读和幻读等问题。然而,由于串行化执行,会牺牲一定的并发性能。

3.3 Spring 中设置隔离级别

  在Spring中,可以使用@Transactional注解设置事务的隔离级别。Spring提供了与数据库事务隔离级别对应的五个常量:

  1. DEFAULT:使用数据库的默认隔离级别。
  2. READ_UNCOMMITTED:对应数据库的读取未提交数据(Read Uncommitted)隔离级别。
  3. READ_COMMITTED:对应数据库的读取已提交数据(Read Committed)隔离级别。
  4. REPEATABLE_READ:对应数据库的可重复读(Repeatable Read)隔离级别。
  5. SERIALIZABLE:对应数据库的可串行化(Serializable)隔离级别。

  使用@Transactional注解时,可以通过isolation属性指定事务的隔离级别。例如:

@Transactional(isolation = Isolation.READ_COMMITTED)
public void myMethod() {
    // 事务处理逻辑
}

Spring 中的事务 和 事务的传播机制

4. Spring 事务传播机制

4.1 什么是事务的传播机制?

  事务传播机制是指定事务在方法调用之间如何传播和影响的机制,通过定义事务的传播行为,控制事务在不同方法之间的创建、挂起、恢复和回滚操作。

Spring 中的事务 和 事务的传播机制

  下面是常见的事务传播行为:

  1. REQUIRED(默认):如果当前存在事务,则加入到当前事务中,如果没有事务,则创建一个新的事务。
  2. SUPPORTS:如果当前存在事务,则加入到当前事务中,如果没有事务,则以非事务的方式执行。
  3. MANDATORY:必须在一个已存在的事务中执行,否则抛出异常。
  4. REQUIRES_NEW:每次都会创建一个新的事务,如果当前存在事务,则将当前事务挂起。
  5. NOT_SUPPORTED:以非事务的方式执行操作,如果当前存在事务,则将当前事务挂起。
  6. NEVER:必须以非事务方式执行,如果当前存在事务,则抛出异常。
  7. NESTED:如果当前存在事务,则在嵌套事务内执行,如果没有事务,则创建一个新的事务。

  "当前存在事务"指的是在方法调用期间已经开启的事务。在Spring中,事务是基于线程的,每个线程都有一个事务上下文。如果在方法调用期间已经存在一个事务上下文(即已经开启了一个事务),则可以说"当前存在事务"。

  当一个方法被调用时,Spring会检查当前线程是否已经有一个事务上下文存在。如果有,那么这个方法就可以在这个已存在的事务上下文中执行,即在当前事务中执行。方法可以访问和操作当前事务中的数据,并共享该事务的一致性和隔离级别(取决于方法的事务传播行为设置)。

  如果当前线程没有事务上下文存在,那么方法可以选择创建一个新的事务,或者以非事务方式执行。这取决于方法的事务传播行为设置。新的事务上下文会在方法开始时创建,并在方法执行完毕后进行提交或回滚。

  例如,一个方法A内部调用了另一个方法B,如果方法B具有REQUIRED(默认)的事务传播行为,而方法A已经在一个事务中执行,那么方法B将加入到方法A的事务中,共同参与事务的操作。

Spring 中的事务 和 事务的传播机制

4.2 事务传播机制的演示

  本篇只演示一部分。

4.2.1 准备工作

  在演示之前,这里先创建两张表,以方便我们看出它们的作用。

Spring 中的事务 和 事务的传播机制

Spring 中的事务 和 事务的传播机制

  插入的数据:

Spring 中的事务 和 事务的传播机制

  log 表为空:

Spring 中的事务 和 事务的传播机制

  定义3个类:

@RestController
@RequestMapping("/user3")
public class UserController3 {

    @Autowired
    private UserService userService;

    // REQUIRED 类型
    @RequestMapping("/add")
    @Transactional(propagation = Propagation.REQUIRED)
    public int add(String username,String password){
        if(username == null || password == null || username.equals("") || password.equals("")){
            return 0;
        }
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername(username);
        userInfo.setPassword(password);
        int result = userService.add(userInfo);
        return result;
    }
}
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private LogService logService;

    public Integer delete(int id){
        return userMapper.delete(id);
    }

    // REQUIRED 类型
    //添加用户
    @Transactional(propagation = Propagation.REQUIRED)
    public Integer add(UserInfo userInfo){
        //给用户表添加用户信息
        int addUserResult = userMapper.add(userInfo);
        System.out.println("添加用户结果:" + addUserResult);
        Log log = new Log();
        log.setMessage("添加日志信息");
        logService.add(log);
        return 0;
    }
}
@Service
public class LogService {

    @Autowired
    private LogMapper logMapper;
    
    //添加日志信息
	// REQUIRED 类型
    @Transactional(propagation = Propagation.REQUIRED)
    public Integer add(Log log){
        int result = logMapper.add(log);
        System.out.println("添加日志的结果:" + result);
        //回滚事务,模仿发生异常,这里为什么不写一个异常呢?因为异常会传递到外面的方法。
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        return result;
    }
}

4.2.2 REQUIRED 演示

  调用关系:

Spring 中的事务 和 事务的传播机制

  传入以下的值:

Spring 中的事务 和 事务的传播机制

Spring 中的事务 和 事务的传播机制   可以看到,两个表都是添加成功的,但是LogService中的add方法回滚了,重点看其它方法回滚了没有:

Spring 中的事务 和 事务的传播机制

  这两个表没有变化,说明所有的方法都是回滚了的。这就体现了REQUIRED这个传播行为,一个方法回滚了,其它所有方法都回滚。

  更具体的:LogService中的add方法本身有事务,UserService中的add方法也是REQUIRED。这时候,LogService中的add就加入了UserService中的事务,相当于一个整体。

4.2.3 REQUIRES_NEW 演示

  将方法都改为REQUIRES_NEW,方法调用跟上面一样。

@RequestMapping("/add")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int add(String username,String password){
    if(username == null || password == null || username.equals("") || password.equals("")){
        return 0;
    }
    UserInfo userInfo = new UserInfo();
    userInfo.setUsername(username);
    userInfo.setPassword(password);
    int result = userService.add(userInfo);
    return result;
}
//添加用户
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer add(UserInfo userInfo){
    //给用户表添加用户信息
    int addUserResult = userMapper.add(userInfo);
    System.out.println("添加用户结果:" + addUserResult);
    Log log = new Log();
    log.setMessage("添加日志信息");
    logService.add(log);
    return 0;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Integer add(Log log){
    int result = logMapper.add(log);
    System.out.println("添加日志的结果:" + result);
    //回滚事务,模仿发生异常,这里为什么不写一个异常呢?因为异常会传递到外面的方法。
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    return result;
}

  发送请求:

Spring 中的事务 和 事务的传播机制

  结果:

Spring 中的事务 和 事务的传播机制

  UserServiceadd方法的事务没有回滚,LogService中的事务回滚了,回忆REQUIRES_NEW:每次都会创建一个新的事务,如果当前存在事务,则将当前事务挂起。在这里,LogService中的事务先执行,执行完后再执行UserService中的事务。

4.2.4 NESTED(嵌套事务)演示

  同样的,把@Transactional改为NESTED

  请求:

Spring 中的事务 和 事务的传播机制

  数据库中的表:

Spring 中的事务 和 事务的传播机制

  LogService中的事务已经回滚,但是嵌套事务不会回滚嵌套之前的事务,也就是说嵌套事务可以实现部分事务回滚,但是这与上面的REQUIRES_NEW是一样的效果呀,它们有什么区别呢?

4.2.5 NESTED(嵌套事务)与 REQUIRES_NEW的区别

  1. NESTED(嵌套事务):

      在嵌套事务中,内部事务实际上是由外部事务开启和提交/回滚的。 当外部事务回滚时,会导致内部事务也被回滚,即使内部事务已经执行了一些提交操作。   这是因为嵌套事务的模拟通过保存和恢复事务状态来实现,当外部事务回滚时,它会回滚到开启内部事务的那个点,包括内部事务执行的任何修改或提交。这样可以确保事务的一致性。

  2. REQUIRES_NEW

  REQUIRES_NEW表示创建一个独立的事务。当一个事务(外部事务)调用另一个带有REQUIRES_NEW传播行为的事务时,内部事务将在一个新的事务中执行,独立于外部事务。内部事务的提交或回滚不会影响外部事务。无论外部事务是否回滚,内部事务都可以独立提交或回滚。

4.3 嵌套事务和加入事务的区别

  1. 嵌套事务(Nested Transactions): 嵌套事务是指在一个事务内部开启了另一个独立的事务。 嵌套事务可以在父事务的范围内执行,并且具有独立的事务日志和回滚机制。 嵌套事务允许在父事务中进行更细粒度的操作和控制,例如,在一个长事务中的某个步骤中开启了一个子事务,子事务可以独立提交或回滚,而不会影响父事务的其他步骤。嵌套事务通常用于复杂的业务逻辑,可以提供更灵活的事务处理。
  2. 加入事务(Join Transactions): 加入事务是指将一个独立的事务合并到当前事务中,使它们成为一个整体。 加入事务可以将多个事务合并为一个更大的事务,确保它们作为一个原子操作进行提交或回滚。加入事务通常用于多个独立事务之间存在逻辑上的依赖关系,需要以一致的方式进行处理。通过将多个事务加入到一个事务中,可以保证它们的一致性,并且要么全部提交成功,要么全部回滚。