目录
事务基本概念
前置准备
Spring Boot 事务使用
编程式事务
声明式事务
@Transactional 注解参数说明
@Transational 对异常的处理
解决方案一
解决方案二
@Transactional 的工作原理
面试题
Spring Boot 事务失效的场景有那些?
事务基本概念
- 事务指一组操作,这些操作要么全部成功,要么全部失败
- 如果在一组操作中有一个操作失败了,那么整个事务便会回滚,即撤销已经执行的操作,从而保证数据的一致性和完整性
实例理解
- 典型实际场景为 银行转账操作
两个步骤
- 从源账户扣除指定金额
- 将该金额添加到目标账户
分析原因
- 这两个步骤必须保证同时执行成功,如果其中任意一个步骤失败,便必须撤销整个操作,以保持数据的一致性
- 即 在扣款成功后,如果存款时发生错误(如网络问题),那么我们必须要回滚扣款操作,以确保不会错误地从源账户中扣款
前置准备
- 下述实例均基于 实现根据用户 id 删除用户信息功能
- 创建一个 user 表,并添加几条用户信息
- 创建 User 实体类 与 数据库的 user 表字段名相对应
import lombok.Data; @Data public class User { private int id; private String name; private int age; private String password; private int state; }
- 初始化 UserMapper 接口,此处我们添加一个 del 方法
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; //添加 @Mapper 注解 代表该接口会伴随这 项目的启动而注入到容器中 @Mapper public interface UserMapper { // 根据用户id 删除用户信息 int del(@Param("user_id") int id); }
- 在与 接口相对应的 XML 文件中,添加上与 del 方法 相对应的 sql 语句
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.UserMapper"> <delete id="del"> delete from user where id = #{user_id} </delete> </mapper>
Spring Boot 事务使用
编程式事务
- Spring Boot 中内置了两个对象
- DataSourceTransactionManager 用来获取事务(开启事务)、提交或回滚事务
- TransactionDefinition 为事务的属性,在获取事务的时候需要将其 传递进去,从而获得一个事务 TransactionStatus
实例理解
- 我们在 UserController 中 使用编程式事务给 根据用户id 删除用户信息 这一功能加上事务
import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.stereotype.Controller; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @ResponseBody @Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; // 编程式 事务 @Autowired private DataSourceTransactionManager transactionManager; @Autowired private TransactionDefinition transactionDefinition; @RequestMapping("/del") public String del(Integer id) { if(id < 0 || id == null) return "请输入正确的用户 id"; TransactionStatus transactionStatus = null; int result = 0; try { // 1. 开启事务 transactionStatus = transactionManager.getTransaction(transactionDefinition); // 2. 业务操作 删除用户 result = userService.del(id); System.out.println("del 方法:" + (result == 1 ? "删除成功": "删除失败" )); // 3. 提交事务 transactionManager.commit(transactionStatus); // 提交事务 }catch (Exception e) { if(transactionStatus != null){ // 发生异常 回滚事务 transactionManager.rollback(transactionStatus); // 回滚事务 } } return "del 方法:" + (result == 1 ? "删除成功": "删除失败" ); } }
测试结果
- 在浏览器的 URL 地址框中输入相对应地址,来调用上述代码的方法
声明式事务
- Spring Boot 提供了 @Transactional 注解实现事务
- 只需在需要的方法上添加 @Transaction 注解即可
- 无需手动开启事务和提交事务,进入方法时自动开启事务,方法执行完会自动提交事务
- 如果中途发生了没有处理的异常会自动回滚事务
注意:
- @Transactional 注解可以用来修饰方法或类
- 修饰方式时,该方法必须为 public 否则不生效
- 修饰类时,表明该注解对该类中所以的 public 方法都生效
实例理解
- 我们在 UserController2 中 使用声明式事务给 根据用户id 删除用户信息 这一功能加上事务
import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user2") public class UserController2 { @Autowired private UserService userService; @RequestMapping("/del") @Transactional public String del(Integer id) { if(id < 0 || id == null) return "请输入正确的用户 id"; int result = userService.del(id); return "del 方法:" + (result == 1 ? "删除成功": "删除失败" ); } }
测试结果
- 在浏览器的 URL 地址框中输入相对应地址,来调用上述代码的方法
@Transactional 注解参数说明
参数 作用 value 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器 transactionManager 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器 propagation 事务的传播行为,默认值为 Propagation.REQUIRED isolation 事务的隔离级别,默认值为 Isolation.DEFAULT timeout 事务的超时时间,默认值为 -1 如果超过该事件限制但事务还没有完成则自动回滚事务 readOnly 指定事务是否为只读事务 默认值为 false 为了忽略那些不需要事务的方法 比如读取事务 rollbackFor 用于指定能够触发事务回滚的异常类型 可以指定多个异常类型 rollbackForClassName 用于指定能够触发事务回滚的异常类型 可以指定多个异常类型 noRollbackFor 抛出指定的异常类型,不会滚事务,也可以指定多个异常类型 noRollbackForClassName 抛出指定的异常类型,不会滚事务,也可以指定多个异常类型 注意:
- 区别 只读事务 和 无事务
- 只读事务 可以设置隔离级别,默认为 repeatable read ,可设置 isolation 更改隔离级别
- 无事务 仅为默认的隔离级别 repeatable read
@Transational 对异常的处理
实例理解
- 此处我们故意在 UserController 中加入异常代码,并手动捕获该 算数异常
- 那么此处 @Transational 是否会回滚 del 操作呢?
package com.example.demo.controller; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user2") public class UserController2 { @Autowired private UserService userService; @RequestMapping("/del") @Transactional // 在方法执行前开启事务 方法正常执行完后提交事务 执行途中发生异常回滚事务 public String del(Integer id) { if(id < 0 || id == null) return "请输入正确的用户 id"; int result = userService.del(id); try { int num = 10/0; } catch (Exception e) { e.printStackTrace(); } return "del 方法:" + (result == 1 ? "删除成功": "删除失败" ); } }
执行结果:
- 在浏览器的 URL 地址框中输入相对应地址,来调用上述代码的方法
- 由上图可知此处我们的 @Transational 并未回滚 del 操作
- 即 @Transactional 在异常被捕获的情况下,不会进行事务的自动回滚
解决方案一
- 捕获到异常后,再重新抛出异常,让框架感知到异常,如果框架感知到异常,便会自动回滚事务
@RequestMapping("/del") @Transactional // 在方法执行前开启事务 方法正常执行完后提交事务 执行途中发生异常回滚事务public String del(Integer id) { if(id < 0 || id == null) return "请输入正确的用户 id"; int result = userService.del(id); try { int num = 10/0; } catch (Exception e) { e.printStackTrace(); // 抛出异常 throw e; } return "del 方法:" + (result == 1 ? "删除成功": "删除失败" ); }
执行结果:
- 在浏览器的 URL 地址框中输入相对应地址,来调用上述代码的方法
- 数据库中 id = 1 的 xiaolin 未被删除,说明此时 @Transational 进行了回滚操作
解决方案二
- 捕获到异常后,手动回滚事务,此处框架是感知不到异常的
@RequestMapping("/del") @Transactional // 在方法执行前开启事务 方法正常执行完后提交事务 执行途中发生异常回滚事务public String del(Integer id) { if(id < 0 || id == null) return "请输入正确的用户 id"; int result = userService.del(id); try { int num = 10/0; } catch (Exception e) { e.printStackTrace(); // 手动回滚事务 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } return "del 方法:" + (result == 1 ? "删除成功": "删除失败" ); }
- TransactionAspectSupport 为 Spring 框架中的一个类,提供了事务相关的支持和功能
- currentTransactionStatus 为 TransactionAspectSupport 类的一个静态方法,用于获取当前事务的状态对象
- setRollbackOnly 为 TransactionStatus 接口的一个方法,用于将当前事务标记为回滚状态0
执行结果:
- 在浏览器的 URL 地址框中输入相对应地址,来调用上述代码的方法
- 数据库中 id = 1 的 xiaolin 未被删除,说明此时 @Transational 进行了回滚操作
重点理解
- 此处为什么会返回一个 删除成功?
- 代码从上到下顺序执行,先执行了 del 操作
- 即此处的 result 值已经成功被赋值为 1 (返回值为 del 操作影响的行数)
- 然后我们才对 算数异常进行捕获,捕获之后再进行的 回滚操作
- 且异常捕获之后,我们并未抛出异常,从而不会出现方案一的服务器错误
- 在捕获完异常后代码将继续向下执行,此时便返回 "del 方法:" + (result == 1 ? "删除成功": "删除失败" )
- 因为此处的 result 等于 1,所以返回了一个 删除成功
- 但是我们要明白的是 我们在捕获异常后,在处理异常时进行了事务的回滚
- 所以此处数据库中的 id = 1 的 xiaolin 未被删除
@Transactional 的工作原理
- 此处声明式事务的实现方式 可使用 Spring AOP 来实现
- 执行目标方法之前 先开启事务,类似于前置通知
- 执行完目标方法之后 再提交事务,类似于后置通知
- 如果在执行中途发生了没有处理的异常 便回滚事务
- 综上 我们可以直接将目标方法 写入环绕通知中
/* * 环绕通知 * 此处的 joinPoint 就是连接点,即方法本身 * */ @Around("pointcut()") public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { Object obj = null; System.out.println("执行目标方法之前 这里开启事务"); try { // 此处执行目标方法 obj = joinPoint.proceed(); }catch (Exception e) { System.out.println("执行目标方法出现异常 这里回滚事务"); } System.out.println("执行目标方法之后 这里提交事务"); // 最后将执行的结果交给框架 return obj; }
上述代码仅为 实现声明式事务 的大致思路
面试题
Spring Boot 事务失效的场景有那些?
- @Transactional 修饰的方法为非 public ,导致事务失效
- @Transactional 设置了一个较小的超时时间,如果方法本身的执行时间超过了设置的 timeout 超时时间,那么就会导致本来应该正常插入数据的方法执行失败
- 代码中有 try/catch 语句,仅捕获异常,不进行额外处理,则导致 @Transactional 不会自动回滚事务
- 数据库不支持事务,我们程序中的 @Transactional 只是给调用的数据库发生了:开始事务、提交事务 或 回滚事务 的之类,但是如果数据库本身不支持事务,如 MySQL 中设置了使用 MySAM 引擎,那么它本身是不支持事务的,在这种情况下,即使在程序中添加了 @Transactional 注解,那么依然不会有事务行为
- 当调用类内部的 @Transactional 修饰的方法时,事务是不会生效的
@RequestMapping("/save") public int saveMappping(UserInfo userInfo) { return save(userInfo); } @Transactional public int save(UserInfo userInfo) { // 非空效验 if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername()) || !StringUtils.hasLength(userInfo.getPassword())) return 0; int result = userService.save(userInfo); int num = 10 / 0; // 此处设置一个异常 return result; }