Spring 事务探索

1 Spring事务简介

在Spring框架中,事务(Transaction)是一种用于管理数据库操作的机制,旨在确保数据的一致性、可靠性和完整性。事务可以将一组数据库操作(如插入、更新、删除等)视为一个单独的执行单元,要么全部成功地执行,要么全部回滚。这样可以确保数据库在任何时候都保持一致的状态,即使在发生故障或错误时也能保持数据的完整性。Spring框架通过提供事务管理功能,使开发者能够更轻松地管理事务的边界。

Spring主要提供了两种主要的事务管理方式:
编程式事务管理:通过编写代码显式地管理事务的开始、提交和回滚操作。这种方式提供了更大的灵活性,但也需要更多的代码维护。
声明式事务管理:通过在配置中声明事务的行为,由 Spring 框架自动处理事务的边界,减少了开发者的工作量,并提高了代码的可维护性。

2 Spring 中事务的实现方法

2.1 Spring 编程式事务(手动)

在 Spring 中,编程式事务管理是一种手动控制事务边界的方式,与 MySQL 操作事务的方法类似,它涉及三个重要的操作步骤:
开启事务:首先需要通过获取事务管理器(例如 DataSourceTransactionManager)来获取一个事务,从而开始一个新的事务。事务管理器是用于管理事务的核心组件。
提交事务:一旦一组数据库操作成功执行,并且希望将这些更改永久保存到数据库中,就可以调用事务对象的提交方法。这将使得事务中的所有操作都被应用到数据库。
回滚事务:如果在事务处理过程中发生错误或某种条件不满足,就可以调用事务对象的回滚方法,从而撤销事务中的所有操作,回到事务开始前的状态。

在 Spring 中,可以利用内置的事务管理器 DataSourceTransactionManager 来获取事务,提交或回滚事务。此外,TransactionDefinition 是用来定义事务的属性的,当获取事务时需要将 TransactionDefinition 传递进DataSourceTransactionManager以获取一个事务状态 TransactionStatus。
例如,下面的代码演示了编程式事务:

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

    // 编程式事务
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    @Autowired
    private TransactionDefinition transactionDefinition;

    @Autowired
    private UserService userService;

    @RequestMapping("/delete")
    public int deleteById(@RequestParam("id") Integer id) {
        if (id == null || id < 0) return 0;
        // 1. 开启事务
        TransactionStatus transactionStatus = null;
        int res = 0;
        try {
            transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

            // 2. 业务操作 —— 删除用户
            res = userService.deleteById(id);
            System.out.println("删除: " + res);

            // 3. 提交、回滚事务
            // 提交事务
            dataSourceTransactionManager.commit(transactionStatus);
        } catch (Exception e) {
            e.printStackTrace();

            // 回滚事务
            if (transactionStatus != null) {
                dataSourceTransactionManager.rollback(transactionStatus);
            }
        }
        return res;
    }
}

这段代码展示了如何通过编程式事务管理在Spring中处理用户删除操作。编程式事务允许我们在代码中明确地控制事务的边界,以及在需要时手动提交或回滚事务。

2.2 Spring 声明式事务(自动)

声明式事务的实现非常简单,只需要在需要的方法上添加 @Transactional 注解就可以轻松实现,无需手动开启或提交事务。
a、当进入被注解的方法时,Spring 会自动开启一个事务。
b、方法执行完成后,如果没有抛出未捕获的异常,事务会自动提交,保证数据的一致性。
c、然而,如果方法在执行过程中发生了未经处理的异常,事务会自动回滚,以确保数据库的完整性和一致性。
这种方式大大简化了事务管理的编码,减少了手动处理事务的繁琐操作,提高了代码的可读性和可维护性。例如下面的代码实现:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    // 声明式事务
    @RequestMapping("/delete")
    @Transactional
    public int deleteById(Integer id) {
        if (id == null || id < 0) return 0;
        int result = userService.deleteById(id);
        return result;
    }
}

在这个示例中,deleteById 方法使用了 @Transactional 注解,表示该方法需要受到声明式事务的管理。在这个方法内部,首先检查了传入的 id,如果为负数则直接返回结果。然后,调用了 userService.deleteById(id) 方法,删除了指定用户。在方法结束时,事务会自动提交。
同时,如果在执行过程中发生了未处理的异常,事务将会自动回滚,以保持数据库的一致性。这种方式简化了事务管理,提高了代码的可读性和可维护性。

2.3 Spring 事务失效场景

在某些情况下,Spring 中的事务可能会失效,导致事务不生效或不按预期执行。以下是一些可能导致事务失效的场景:
1、非 public 修饰的方法: 默认情况下,@Transactional 注解只对 public 访问修饰符的方法起作用。如果你在非 public 方法上添加了 @Transactional 注解,事务可能不会生效。
2、timeout 超时:如果事务执行的时间超过了设置的 timeout 值,事务可能会被强制回滚。这可能会导致事务不按预期执行,特别是当事务需要执行较长时间的操作时。
3、代码中有 try/catch: 如果在方法内部捕获并处理了异常,Spring 将无法感知到异常,从而无法触发事务回滚。这可能导致事务在异常发生时不会回滚。
4、调用类内部带有 @Transactional 的方法:当一个类内部的方法被调用时,它的 @Transactional 注解可能不会生效。这是因为 Spring 默认使用基于代理的事务管理,直接在类内部调用方法不会经过代理,从而事务管理可能不会生效。
5、数据库不支持事务:如果你的数据库不支持事务,例如使用了某些特殊的数据库引擎,事务可能无法正常工作。在这种情况下,应该确保使用支持事务的数据库引擎。例如MySQL存储引擎必须是InnoDB,Myisam引擎不支持事务。
6、如果methodA上没有@Transactional注解,但是调用了一个本类的有@Transactional注解的methodB,事务也不会生效,这时候需要借助我们上文提供的SpringUtil从spring上下文中获取实例才可以,因为注解的本质是aop和动态代理。
7、@Transactional 注解的方法所在的类必须被 Spring 管理。这个很好理解,因为我们所使用的声明式注解,本身就是spring,如果不被spring容易托管,是肯定不会生效的。

3 事务的隔离级别

3.1 事务的特性回顾

在数据库中,事务具有以下四个重要的特性,通常被称为 ACID 特性:
原子性(Atomicity): 事务被视为一个不可分割的操作单元,要么全部执行成功,要么全部失败回滚。
一致性(Consistency): 事务使数据库从一个一致的状态转变到另一个一致的状态,保证数据的完整性和一致性。
隔离性(Isolation): 并发执行的事务之间应该互不影响,每个事务都感觉自己在独立地操作数据。
持久性(Durability): 一旦事务提交,其对数据库的修改就应该是永久性的,即使发生系统崩溃也不应该丢失。

3.2 MySQL 的事务隔离级别

MySQL 支持以下四个事务隔离级别,用于控制多个事务之间的相互影响程度:
读未提交(Read Uncommitted): 允许一个事务读取另一个事务尚未提交的数据。这是最低的隔离级别,可能会导致脏读、不可重复读和幻读的问题。
读已提交(Read Committed): 允许一个事务只能读取另一个事务已经提交的数据。这可以避免脏读,但可能会出现不可重复读和幻读的问题。
可重复读(Repeatable Read): 保证在同一个事务中多次读取同样记录的结果是一致的,即使其他事务对该记录进行了修改。这可以避免脏读和不可重复读,但可能出现幻读。
串行化(Serializable): 最高的隔离级别,确保每个事务都完全独立运行,避免了脏读、不可重复读和幻读问题,但可能影响并发性能。
以下是事务四个隔离级别对应的脏读、不可重复读、幻读情况:

隔离级别脏读不可重复读幻读
读未提交
读已提交×
可重复读××
串行化×××

一般而言,数据库的读已提交(READ COMMITTED)能够满足业务绝大部分场景了

3.3 Spring 事务的隔离级别

Spring 通过 @Transactional 注解中的 isolation 参数来支持不同的事务隔离级别。Isolation的源码如下:

可以使用这些枚举值来设置隔离级别:
Isolation.DEFAULT:使用数据库的默认隔离级别。
Isolation.READ_UNCOMMITTED:读未提交。
Isolation.READ_COMMITTED:读已提交。
Isolation.REPEATABLE_READ:可重复读。
Isolation.SERIALIZABLE:串行化。
例如,指定 Spring 事务的隔离级别为 DEFAULT:

@RequestMapping("/delete")
@Transactional(isolation = Isolation.DEFAULT)
public int deleteById(Integer id) {
    if (id == null || id < 0) return 0;
    int result = userService.deleteById(id);
    return result;
}

通过选择合适的事务隔离级别,可以在并发环境中控制事务之间的相互影响程度,从而避免数据不一致的问题。不同的隔离级别在性能和数据一致性方面有不同的权衡,开发人员需要根据具体的业务需求来选择合适的隔离级别。

4 Spring 事务的传播机制

4.1 为什么需要事务传播机制

在复杂的应用场景中,一个事务操作可能会调用多个方法或服务。这些方法可能需要独立地进行事务管理,但又需要协同工作,以保持数据的一致性和完整性。这时就需要引入事务传播机制。

事务传播机制定义了多个事务方法之间如何协同工作,如何共享同一个事务,以及在嵌套事务中如何进行隔离和提交。通过事务传播机制,可以确保多个事务方法在执行时能够按照一定的规则进行协调,避免数据不一致的问题。

4.2 事务传播机制的分类

Spring 定义了七种事务传播行为,用于控制多个事务方法之间的交互。这些传播行为可以在 @Transactional 注解中的 propagation 参数中进行设置。以下是这些传播行为:
REQUIRED(默认): 如果当前存在事务,就加入到当前事务中;如果没有事务,就创建一个新的事务。这是最常用的传播行为。
SUPPORTS: 如果当前存在事务,就加入到当前事务中;如果没有事务,就以非事务方式执行。
MANDATORY: 如果当前存在事务,就加入到当前事务中;如果没有事务,就抛出异常。
REQUIRES_NEW: 无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起。
NOT_SUPPORTED: 以非事务方式执行,如果当前存在事务,就将当前事务挂起。
NEVER: 以非事务方式执行,如果当前存在事务,就抛出异常。
NESTED: 如果当前存在事务,就在一个嵌套的事务中执行;如果没有事务,就与 REQUIRED 一样。

以上 7 种传播行为,可以根据是否支持当前事务分为以下 3 类:

4.3 Spring 事务传播机制使用案例

REQUIRED 和 NESTED 传播机制的事务演示:

控制层 UserController :

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

    @Autowired
    private UserService userService;

    @RequestMapping("/add") // /add?username=lisi&password=123456
    @Transactional(propagation = Propagation.NESTED)
    //@Transactional(propagation = Propagation.REQUIRED)
    //@Transactional(propagation = Propagation.REQUIRES_NEW)
    public int add(@RequestParam("username") String username, @RequestParam("password") String password) {
        if (null == username || null == password || "".equals(username) || "".equals(password)) {
            return 0;
        }
        int result = 0;
        // 用户添加操作
        UserInfo user = new UserInfo();
        user.setUsername(username);
        user.setPassword(password);
        result = userService.add(user);

        try {
            int num = 10 / 0; // 加入事务:外部事务回滚,内部事务也会回滚
        } catch (Exception e) {
            e.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }

        return result;
    }
}

服务层 UserService:

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private LogService logService;

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

    @Transactional(propagation = Propagation.NESTED)
    //@Transactional(propagation = Propagation.REQUIRED)
    //@Transactional(propagation = Propagation.REQUIRES_NEW)
    public int add(UserInfo user){
        // 添加用户信息
        int addUserResult = userMapper.add(user);
        System.out.println("添加用户结果:" + addUserResult);

        //添加日志信息
        Log log = new Log();
        log.setMessage("添加用户信息");
        logService.add(log);

        return addUserResult;
    }
}

服务层 LogService:

@Service
public class LogService {
    @Autowired
    private LogMapper logMapper;

    @Transactional(propagation = Propagation.NESTED)
    //@Transactional(propagation = Propagation.REQUIRED)
    //@Transactional(propagation = Propagation.REQUIRES_NEW)
    public int add(Log log){
        int result =  logMapper.add(log);
        System.out.println("添加日志结果:" + result);
        // 模拟异常情况
        try {
            int num = 10 / 0;
        } catch (Exception e) {
            // 加入事务:内部事务回滚,外部事务也会回滚,并且会抛异常
            // 嵌套事务:内部事务回滚,不影响外部事务
            e.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return result;
    }
}

在事务传播机制中,REQUIRED 和 NESTED 是两种不同的传播行为,它们在事务的嵌套、回滚以及对外部事务的影响等方面有所不同。通过上面代码的演示,可以得出 REQUIRED 和 NESTED 之间的主要区别如下:

嵌套性质:
        REQUIRED:内部方法与外部方法共享同一个事务,内部方法的事务操作是外部方法事务的一部分。
        NESTED:内部方法创建一个嵌套事务,它是外部事务的子事务,具有独立的事务状态,内部事务的回滚不会影响外部事务。

回滚行为:
        REQUIRED:如果内部方法抛出异常或设置回滚,会导致整个外部事务回滚,包括内部方法和外部方法的操作。
        NESTED:如果内部方法抛出异常或设置回滚,只会回滚内部事务,而外部事务仍然可以继续执行。

影响外部事务:
        REQUIRED:内部方法的事务操作会影响外部事务的状态,内部方法回滚会导致外部事务回滚。
        NESTED:内部方法的事务操作不会影响外部事务的状态,内部方法回滚不会影响外部事务的提交或回滚。

支持性:
        REQUIRED:较为常用,适用于将多个方法的操作作为一个整体进行事务管理的情况。
        NESTED:在某些数据库中不支持,需要数据库支持保存点(Savepoint)的功能。

总的来说,REQUIRED 适用于需要将多个方法的操作作为一个整体事务管理的情况,而 NESTED 适用于需要在内部方法中创建嵌套事务的情况,保持内部事务的独立性,不影响外部事务。选择使用哪种传播行为取决于业务需求和数据库的支持情况。
 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值