Spring 事务和事务的传播机制

奋斗吧
奋斗吧
擅长邻域:未填写

标签: Spring 事务和事务的传播机制

2023-07-27 18:23:28 410浏览

将一组操作封装成一个执行单元 (分装到一起) , 要么全部成功 , 要么全部失败.Spring 事务的传播机制是指多个事务方法相互调用(嵌套)的情况下 , 如何管理这些事务的提交和回滚.

1. 回顾复习

通过数据库的学习 , 让我们对事务的理解有一定的认识 , 万变不利其宗 , Spring 事务的思想与数据库基本一致.

  • 事务定义

将一组操作封装成一个执行单元 (分装到一起) , 要么全部成功 , 要么全部失败.

  • 为什么要用事务

例如转账时分为两个操作:

第一步: A 账户 - 100元

第二步: B 账户 +100元

如果没有事务 , 第一步执行成功第二步执行失败 , 那么 A账户转出的 100 元就凭空蒸发了. 如果使用事务将两个操作分装为一个执行单元 , 那么这一组操作要么全部成功 , 要么全部失败.

  • MySQL 中事务的使用

事务在 MySQL 中有 3 个重要的操作: 开启事务 , 提交事务 , 回滚事务 , 对应的操作命令如下:

-- 开启事务
start transaction;
-- 业务执行

-- 提交事务
commit;

-- 回滚事务
rollback;

2. Spring 中事务的实现

Spring 中事务的操作分为两类:

  1. 编程式事务 (手动写代码操作事务)
  2. 声明式事务 (利用注解自动开启和提交事务)

虽然在企业级开发中基本都使用 声明式事务, 但编程式事务的掌握也是不可或缺的.

2.1 编程式事务

Spring 手动操作事务和上面 MySQL 操作事务类似 , 也是有三个重要的操作步骤:

  • 开启事务
  • 提交事务
  • 回滚事务

SpringBoot 内置两个对象 , DataSourceTransactionManager 用来开启事务 , 提交 , 回滚事务 , 而 TransactionDefinition 是事务的属性 , 在开启事务的时候需要将 TransactionDefinition 传递进去从而获得一个事务 TransactionStatus , 实现代码如下:

RequestMapping("/user")
@RestController
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){
//            1. 开启事务
            TransactionStatus transactionStatus = null;
//             业务操作 , 删除用户
            int result = 0;
            try {
                transactionStatus = transactionManager.getTransaction(transactionDefinition);
                result = userService.del(id);
                System.out.println("删除: " + result);
//            2. 提交事务 /回滚事务
            transactionManager.commit(transactionStatus);//提交事务
            } catch (Exception e) {
                if (transactionStatus != null){
                    transactionManager.rollback(transactionStatus);
                }
            }
            return result;
        }
        return 0;
    }

从上述代码中可以看出 , 编程式事务实现业务流程非常复杂 , 因此有了声明式事务.

2.2 声明式事务 (自动)

声明式事务极大地 简化了业务的流程 , 只需在需要的方法上添加 @Transactional 注解即可 , 无需手动开启和提交事务 , 进入方法时自动开启事务 , 方法执行完会自动提交事务 , 如果中途出现没有处理的异常 , 将会自动回滚事务 , 具体实现代码如下:

@RestController
@RequestMapping("/user2")
public class UserController2 {
    @Autowired
    private UserService userService;

    @RequestMapping("/del")
    @Transactional// 方法开始之前开始事务 , 方法正常执行结束之后提交事务 , 如果执行途中发生异常, 则回滚异常.
    public int del(Integer id) {
        if (id == null || id <= 0) return 0;
        int result = userService.del(id);
        int num = 10 / 0;
        return result;
    }
}

2.2.1 Transactional 作用范围

@Transactional 可以用来修饰方法或类

  • 修饰方法时: 只能应用到 public 方法上 , 否则不生效.(推荐使用此方法)
  • 修饰类时: 表名注解对该类的所有 public 方法都生效.

2.2.2 @Transactional 参数说明

参数 作用
value 当配置多个事务管理器时 , 可以使用该属性指定选择哪个事务管理器
transactionManager 当配置多个事务管理器时 , 可以使用该属性指定选择哪个事务管理器
propagation 事务的传播行为 , 默认值为 Propagation.REQUIRED
islotion 事务的隔离级别 , 默认值为 Isolation.DEFAULT
timeout 事务的超时时间 , 默认值为 -1 , 如果超过该时间限制但事务还没有完成 , 则自动回滚事务
readOnly 指定事务是否为只读事务 , 默认值为 false.
rollbackFor 用于指定能够触发事务回滚的异常类型 , 可以指定多个异常类型
rollbackForClassName 用于指定能够触发事务回滚的异常类型 , 可以指定多个异常类型
noRollbackFor 抛出指定的异常类型, 不会滚事务 , 也可以指定多个异常类型
noRollbackForClassName 抛出指定的异常类型 , 不会滚事务 , 也可以指定多个异常类型

2.2.3 @Transactional 工作原理

@Transactional 是基于AOP 实现的 , AOP 又是通过动态代理实现的. 如果目标对象实现了接口 , 默认情况下会采用 JDK 的动态代理 , 如果目标对象没有实现接口 , 会使用 CGLIB 动态代理. @Transactional 在开始执行业务之前 , 通过代理先开启事务 , 执行成功之后提交事务 , 中途出现异常则回滚.

@Transactional 实现思路预览:

image-20230721095430420

@Transactional 具体执行细节如下:

image-20230721100556138


3. SpringBoot 事务失效的场景

3.1 非 public 方法

@RestController
@RequestMapping("/user2")
public class UserController2 {
    @Autowired
    private UserService userService;

    @RequestMapping("/del")
    @Transactional// 方法开始之前开始事务 , 方法正常执行结束之后提交事务 , 如果执行途中发生异常, 则回滚异常.
    int del(Integer id) {
        if (id == null || id <= 0) return 0;
//        执行删除方法的操作
        int result = userService.del(id);
//        这里设置一个异常
        int len = 10 / 0;
        return result;
    }
}

执行结果如下:

image-20230719152549140

通过查询数据库 , 可以发现发生异常后 , 事务并没有出现回滚操作.

image-20230719152921406

3.2 timeout 超时

如果我们给 @Transactional 设置了一个较小的超时时间 . 如果方法执行时间超过了设置的 timeout 超时时间 , 那么就会导致事务无法正常执行.

@RestController
@RequestMapping("/user2")
public class UserController2 {
    @Autowired
    private UserService userService;

    @RequestMapping("/del")
    @Transactional(timeout = 3)// 超时时间为 3 s
    public int del(Integer id) throws InterruptedException {
        if (id == null || id <= 0) return 0;
//        执行删除方法的操作
        int result = userService.del(id);
        return result;
    }
}

UserService 中的 del 方法实现如下:

    public int del(Integer id) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);//休眠时间为 5 s
        return userMapper.del(id);
    }

程序执行结果如下:

image-20230719153614542

数据库并未执行删除操作:

image-20230719153932779

3.3 代码中有 try/catch

上文中我们提到, 如果public 方法被 @Transactional 修饰 , 当方法中出现异常之后 , 事务会自动回滚. 然而在程序中加了 try/catch之后 , @Transactional 就不会自动回滚事务了 , 如果在方法内部使用了 try/catch 块来捕获异常并处理,Spring 就无法感知到异常的发生,因此无法触发事务回滚

@RestController
@RequestMapping("/user2")
public class UserController2 {
    @Autowired
    private UserService userService;

    @RequestMapping("/del")
    @Transactional(timeout = 3)// 超时时间为 3 s
    public int del(Integer id) throws InterruptedException {
        if (id == null || id <= 0) return 0;
//        执行删除方法的操作
        int result = userService.del(id);
//        这里设置一个异常
        try {
            int len = 10 / 0;
        } catch (Exception e) {
            
        }
        return result;
    }
}

解决方案一: 抛出异常

@RestController
@RequestMapping("/user2")
public class UserController2 {
    @Autowired
    private UserService userService;

    @RequestMapping("/del")
    @Transactional(timeout = 3)// 超时时间为 3 s
    public int del(Integer id) throws InterruptedException {
        if (id == null || id <= 0) return 0;
//        执行删除方法的操作
        int result = userService.del(id);
//        这里设置一个异常
        try {
            int len = 10 / 0;
        } catch (Exception e) {
            throw e;
        }
        return result;
    }
}

解决方案二: 手动回滚事务

@RestController
@RequestMapping("/user2")
public class UserController2 {
    @Autowired
    private UserService userService;

    @RequestMapping("/del")
    @Transactional(timeout = 3)// 超时时间为 3 s
    public int del(Integer id) throws InterruptedException {
        if (id == null || id <= 0) return 0;
//        执行删除方法的操作
        int result = userService.del(id);
//        这里设置一个异常
        try {
            int len = 10 / 0;
        } catch (Exception e) {
//            TransactionAspectSupport.currentTransactionStatus获取当前事务
//            setRollbackOnly 手动设置回滚事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return result;
    }
}

3.4 调用类内部的 @Transactiona方法

@RestController
@RequestMapping("/user2")
public class UserController2 {
    @Autowired
    private UserService userService;

    @RequestMapping("/del")
    public int delMapping(Integer id){
        return del(id);
    }
    @Transactional()
    int del(Integer id)  {
        if (id == null || id <= 0) return 0;
//        执行删除方法的操作
        int result = userService.del(id);
//        这里设置一个异常
        int len = 10 / 0;
        return result;
    }
}

当调用类内部的 @Transactional 修饰的方法时,事务不会生效的原因是因为 Spring 使用了基于代理的 AOP(Aspect-Oriented Programming)机制来实现 @Transactional 的功能,在运行时生成代理对象来增强被代理的目标对象。当目标对象的方法被调用时,代理对象会拦截方法的执行,并在方法执行前后添加额外的逻辑,比如开启和提交事务。

然而,这种代理机制只能拦截从外部调用目标对象的方法,而无法拦截目标对象内部方法的调用。因此,当在类内部调用被 @Transactional 修饰的方法时,Spring AOP 无法拦截该方法的执行,事务也就无法生效。

为了解决这个问题,可以将被 @Transactional 修饰的方法抽取到另一个类中,并通过依赖注入的方式将该类注入到目标类中。这样,当目标类内部调用被 @Transactional 修饰的方法时,实际上是通过代理对象来调用,事务就能够生效。

3.5 数据库不支持事务

程序中的 @Transactional 只是给数据库发送了: 开启事务 , 提交事务, 和回滚事务的指令. 如果数据库本身不支持事务 , 例如 Mysql 使用 MyISAM 引擎.


4. 事务的隔离级别

4.1 事务特性回顾

事务有四大特性 (ACID) , 原子性 , 一致性 , 持久性 , 隔离性

  • 原子性: 一个事务中的所有操作 , 要么全部完成 , 要么全部失败. 事务在执行过程中发生错误 , 会被 Rollback 到事务的最初状态.
  • 一致性: 在事务开始之前和事务结束之后 , 数据库的完整性没有被破坏. 这表明写入的资料必须符合所有的规则 , 包括资料的精确度 , 串联性 , 以及后续数据库可以自发的完成预定的工作.
  • 持久性: 事务处理结束之后 , 对数据的修改是永久的 , 即使系统故障也不会丢失.
  • 隔离性: 数据库允许多个并发事务同时对其数据进行读写和修改的能力 , 隔离性可以防止多个事务并发执行时 , 由于交叉执行而导致数据的不一致. 事务的隔离级别分为不同级别 , 包括读未提交(read uncommitted) , 读已提交(read committed) , 可重复读 (repeatable read) , 和串行化 (serializable)

而这四种属性中 , 只有隔离性是可以设置的

为什么要设置事务的隔离级别?

为了保证多个事务的并发执行更可控 , 更符合操作者预期.

4.2 Spring 中设置事务的隔离级别

Spring 中事务的隔离级别可以通过 @Transactional 中的 isolation 属性进行设置 , 具体代码如下:

image-20230721102635585

4.2.1 MySQL 的事物隔离级别

  1. READ UNCOMMITTED: 读未提交 , 该隔离级别中的事务可以看到其他事务中未提交的数据. 而未提交的数据可能会发生回滚 , 因此我们把该级别读到的数据称为脏数据 , 把这个问题称为脏读.
  2. READ COMMITTED: 读已提交 , 该隔离级别的事务只能读到已经提交事务的数据 , 因此不会有脏读的问题. 但由于事务的执行中可以读取到其他事务提交的结果 , 所有在不同时间相同的 SQL 查询中 , 可能会得到不同的结果 , 这种现象叫做不可重复读.
  3. REPEATABLE READ:可重复读 , MySQL 的默认事务隔离级别 , 它能确保多次事务的查询结果一致. 但也会有新问题 , 比如此级别的事务正在执行时 , 另一事务成功的插入了某条数据 , 但因为它每次查询结果都是一致的 , 所以会导致查询不到这条数据 , 自己插入同一条数据又失败了(唯一性约束). 明明事务中查询不到这条数据 , 但自己就是插入不进去 , 这就叫做幻读(Phantom Read).
  4. SEARIALIZABLE: 序列化 , 事务的最高隔离级别 , 他会强制事务排序 , 使之不会发生冲突 , 从而解决了脏读 , 不可重复读和幻读问题 , 但由于执行效率低 , 所以真正使用的场景并不多.
事务隔离级别 脏读 不可重复读 幻读
读未提交(READ UNCOMMITTED)
读已提交(READ COMMITTED) ×
可重复读(REPRATABLE READ) × ×
串行化(SERIALIZABLE) × × ×
  • 脏读: 一个事务读取了另一个事务修改后的数据 , 后一个事务进行了回滚 , 导致第一个事务读到的数据是错误的 .
  • 不可重复读: 同一事务不同时间查询到的结果不同 , 因为两次查询中间有另一个事务把数据修改了.
  • 幻读: 同一事务两次查询同一范围的记录 , 但第二次查询却发现了新记录 .

数据库中可通过以下 SQL 查询全局事务隔离级别和当前连接的事务的隔离级别:

select @@global.tx_isolation,@@tx_isolation;

4.2.2 Spring 事务的隔离级别有 5 种

  1. Isolation.DEFAULT: 以连接的数据库的事务的隔离级别为主.
  2. Isolation.READ_UNCOMMITTED: 读未已提交 , 可以读到未提交的事务 , 存在脏读.
  3. Isolation.READ_COMMITTED: 读已提交 , 只能读到已提交的事务 , 解决了脏读 , 存在不可重复读.
  4. Isolation.REPEATABLE_READ: 可重复读 , 解决了不可重复读 , 但存在幻读.(MySQL 的默认级别).
  5. IsolationSERIALIZABLE: 串行化 , 可以解决所有并发问题 , 但性能太低.

由此可见 Spring 中事务的隔离级别只是比 MySQL 多了一个 Isolation.DEFAUL


5. Spring 事务的传播机制

5.1 定义

Spring 事务的传播机制是指多个事务方法相互调用(嵌套)的情况下 , 如何管理这些事务的提交和回滚.

image-20230722094834302

5.2 加入事务和嵌套事务有什么区别?

  • Propagation.REQUIRED 是默认的传播行为 , 方法调用将加入当前事务 , 若没有则创建一个事务.
  • Propagation.NESTED 是嵌套的传播行为 , 方法调用将在独立子事务中执行 , 具有自己的保存点 , 可独立于外部事务进行回滚 , 而不影响外部事务.

如果你希望内部方法能够独立于外部方法独自回滚则Propagation.NESTED , 如果你希望内部方法和外部方法一起回滚提交则Propagation.REQUIRE

5.2 Spring 事务传播机制使用和各种场景演示

5.2.1 支持当前事务 (REQUIRED)

为了达到多个事务之间的调用效果 , 我们通过 UserController 去调用 UserSerivce 和 LogService , 实现在添加用户的同时记录日志. 事务的传播机制定为 Propagation.REQUIRED , 由于我们在 UserService 中调用 LogService . 那么 UserService 就相当于外部事务 , 而 LogService 就是内部事务. 我们可以分别检测 , 当内部事务回滚时对外部事务的影响和外部事务回滚时对内部事务的影响.

@RestController
@RequestMapping("/user3")
public class UserController3 {
    @Autowired
    private UserService userService;

    @RequestMapping("/add")
    @Transactional(propagation = Propagation.REQUIRED)
    public int add(String username, String password) {
        if (null == username || null == password
                || username.equals("") || username.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;

    @Transactional(propagation = Propagation.REQUIRED)
    public int add(Userinfo userinfo) {
//        给用户表添加用户信息
        int addUserResult = userMapper.add(userinfo);
        System.out.println("添加用户结果: " + addUserResult);
//        添加日志信息
        Log log = new Log();
        log.setMessage("添加用户信息");
        logService.add(log);
        return addUserResult;
    }
}

内部事务设置回滚操作:

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

    @Transactional(propagation = Propagation.REQUIRED)
    public int add(Log log) {
        int result = logMapper.add(log);
        System.out.println("添加日志结果: " + result);
//        回滚
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        return result;
    }
}

程序执行结果如下:

image-20230722150818256

查询数据库:

image-20230722145734193

由此可以发现 , 在 REQUIRED 的传播机制下 , LogService 事务执行回滚会导致 , 整体事务都执行回滚操作. 由于 LogService 为内部事务 , 当外部事务发现内部事务回滚后 , 默认内部事务出现异常 , 由此程序执行结果为异常.

外部事务设置回滚操作:

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

    @Transactional(propagation = Propagation.REQUIRED)
    public int add(Userinfo userinfo) {
//        给用户表添加用户信息
        int addUserResult = userMapper.add(userinfo);
        System.out.println("添加用户结果: " + addUserResult);
      //添加回滚操作
      	TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
//        添加日志信息
        Log log = new Log();
        log.setMessage("添加用户信息");
        logService.add(log);
        return addUserResult;
    }
}

程序执行结果如下:

image-20230722150647235

数据库执行结果如下:

image-20230722150848531

综上 , 当事务的传播机制为 REQUIRED 时 , 无论加入的事务的内部事务还是外部事务 , 都会执行回滚操作.

5.2.2 不支持当前事务(REQUIRES_NEW)

将 UserController , UserService 和 LogService 中的传播机制改为 REQUIRES_NEW.

@RestController
@RequestMapping("/user3")
public class UserController3 {
    @Autowired
    private UserService userService;

    @RequestMapping("/add")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int add(String username, String password) {
        if (null == username || null == password
                || username.equals("") || username.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;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int add(Userinfo userinfo) {
//        给用户表添加用户信息
        int addUserResult = userMapper.add(userinfo);
        System.out.println("添加用户结果: " + addUserResult);
//        添加日志信息
        Log log = new Log();
        log.setMessage("添加用户信息");
        logService.add(log);
        return addUserResult;
    }
}
@Service
public class LogService {
    @Autowired
    private LogMapper logMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int add(Log log) {
        int result = logMapper.add(log);
        System.out.println("添加日志结果: " + result);
//        回滚
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        return result;
    }
}

程序执行结果如下:

image-20230722151843609

数据库执行结果如下:

image-20230722151917434

综上: REUQIRES_NEW 会单独创建一个事务 , 单独的事务发生回滚对其他事务没有影响.

5.2.3 NESTED 嵌套

将所有的事务传播机制改为 NESTED , 原代码不变.

程序执行结果:

image-20230722164247293

数据库结果:

image-20230722164237533

综上: NESTED 会将新事务嵌套进原事务 , 嵌套的事务回滚不会影响到原事务 , 但嵌套事务中如果出现未处理的异常 , 整个程序都会抛异常.

NESTED 嵌套事务之所以能够实现部分回滚的操作 , 是因为事务中存在保存点(savepoint)这个概念 , 嵌套事务相当于建立了一个新的保存点 , 回滚只会回滚到当前保存点的位置 , 因此之前的事务是不受影响的.

好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695