资讯

精准传达 • 有效沟通

从品牌网站建设到网络营销策划,从策略到执行的一站式服务

Spring事务和事务的传播机制-创新互联

1.Spring 中事务的实现方式

Spring 中的操作主要分为两类:

在甘泉等地区,都构建了全面的区域性战略布局,加强发展的系统性、市场前瞻性、产品创新能力,以专注、极致的服务理念,为客户提供成都网站建设、网站设计 网站设计制作定制网站建设,公司网站建设,企业网站建设,成都品牌网站建设,成都全网营销推广,外贸营销网站建设,甘泉网站建设费用合理。
  • 编程式事务 (了解)

  • 声明式事务

编程式事务就是手写代码操作事务, 而声明式事务是利用注解来自动开启和提交事务. 并且编程式事务用几乎不怎么用. 这就好比汽车的手动挡和自动挡, 如果有足够的的钱, 大部分人应该都会选择自动挡.
声明式事务也是如此, 它不仅好用, 还特别方便.

1.1 Spring 编程式事务 (了解)

编程式事务和 MySQL 中操作事务类似, 也是三个重要步骤:

  1. 开启事务

  1. 提交事务

  1. 回滚事务

【代码实现】

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

    @Autowired  // JDBC 事务管理器
    private DataSourceTransactionManager dataSourceTransactionManager;

    @Autowired  // 定义事务属性
    private TransactionDefinition transactionDefinition;

    @RequestMapping("/add")
    public int add(String username, String password) {
        // 非空校验
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return 0;
        }
        // 事务 [得到并开启事务]
        TransactionStatus transactionStatus =
                dataSourceTransactionManager.getTransaction(transactionDefinition);

        int result = userService.add(username, password, null);
        System.out.println("添加影响行数: " + result);

        // 提交事务 or 回滚
        dataSourceTransactionManager.rollback(transactionStatus);
        //  dataSourceTransactionManager.commit(transactionStatus);
        return result;
    }
}
DataSourceTransactionManager 和 TransactionDefinition 是 SpringBoot 内置的两个对象.
DataSourceTransactionManager : 用来获取事务(开启事务)、提交或回滚事务.
TransactionDefinition : 它是事务的属性,在获取事务的时候需要将这个对象传递进去从而获得⼀个事务 TransactionStatus.

【测试编式事务】

上述代码的主要要业务逻辑就是基于 MyBatis实现了一个新增方法, 接下来我们测试一下编程式事务中的回滚操作是否生效.

  1. 在测试之前先查看一下数据库中的用户信息 (userinfo) :

  1. 启动程序后, 在浏览器输入: 127.0.0.1:8080/user/add?username=李华&password=123

3. 此时我们看见控制台显示添加数据成功, 那么要知道代码中的回滚是否生效, 需要查看数据库是否真正的把数据添加进去了.

4. 发现数据库中并没有添加数据, 说明回滚操作生效了. 而提交事务 commit 就和普通的添加操作差不多, 下来可以自己试一下.

1.2 Spring 声明式事务

声明式事务的实现相较于编程式事务来说, 就要简单太多了, 只需要在需要的方法上添加 @Transactional注解就可以实现了.

@Transactional 注解的作用:

当进入方法的时候, 它就会自动开启事务, 当方法结束后, 它就会自动提交事务. 说白了它就是 AOP 的一个环绕通知. 只要加了 @Transactional 注解的方法, 都有一个事务的 AOP , 这都是 Spring 帮我们封装好的.

@Transactional 注解的执行流程:

1. 方法执行之前, 先开启事务, 当方法成功执行完成之后会自动提交事务.
2. 如果方法在执行过程中发生了异常, 那么事务会自动回滚.

【代码实现】

   @RequestMapping("/add2")
    @Transactional // 声明式事务
    public int add2(String username, String password) {
        // 非空校验
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return 0;
        }
        int result = userService.add(username, password, null);
        System.out.println("添加影响行数: " + result);
        return result;
    }

对于方法执行成功的情况就不测试了, 它和普通的插入数据没有多大区别, 重点在于理解 @Transactional注解的含义和作用即可.

【异常情况一】

   @RequestMapping("/add2")
    @Transactional // 声明式事务
    public int add2(String username, String password) {
        // 非空校验
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return 0;
        }
        int result = userService.add(username, password, null);
        System.out.println("添加影响行数: " + result);
        int num = 10 / 0;
        return result;
    }

当我们写出 int num = 10 / 0; 这样一条语句的时候, 看看 @Transactional 是否会进行回滚操作:

启动程序, 浏览器访问 : 127.0.0.1:8080/user/add2?username=王五&password=123

此时程序已经报错了, 并且打印了添加成功语句, 是否真正添加成功, 还是说进行了回滚操作, 就要查询数据库:

发现数据库中并没有王五这条数据, 说明在发生异常的时候, @Transactional 注解帮我们做了回滚操作.

【异常情况二】

对于上述代码抛出异常后, @Transactional 注解帮我们进行回滚, 这一点很好理解, 那么如果我们将这个异常捕获了, @Transactional 注解是否还会进行回滚操作呢 >>

   @RequestMapping("/add2")
    @Transactional // 声明式事务
    public int add2(String username, String password) {
        // 非空校验
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return 0;
        }
        int result = userService.add(username, password, null);
        System.out.println("添加影响行数: " + result);
        try {
            int num = 10 / 0;
        } catch (Exception e) {
            
        }
        return result;
    }

执行结果: 此时程序没有发生报错了.

为了验证是否进行回滚, 继续查询数据库

此时我们发现, @Transactional 注解并没有进行回滚操作, 而是提交了事务. 这是为什么 ??

因为当我们捕捉到异常的时候, Spring 框架会认为我们有能力处理, 所以就不会进行回滚, 而当发生异常我们不处理的时候, Spring 框架就会采取保守的做法, 他知道我们没有能力去处理这个异常, 所以就会帮我们回滚. 所以当出现异常的时候, 我们要根据这个异常是否被处理来判断最终是提交数据了, 还是进了回滚操作.

1.2.1 声明式事务的手动回滚

当第二种异常情况, 捕获异常之后, 事务并没有进行回滚, 我们是需要做出一些处理的. 既然程序发生了异常, 我们一般就需要进行回滚操作的. 对于这种捕获异常的情况,我们有两种方式进行回滚:

  • 将异常继续抛出.

  • 通过代码手动回滚事务.

【代码示例】- 将异常继续抛出

   @RequestMapping("/add2")
    @Transactional // 声明式事务
    public int add2(String username, String password) {
        // 非空校验
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return 0;
        }
        int result = userService.add(username, password, null);
        System.out.println("添加影响行数: " + result);
        try {
            int num = 10 / 0;
        } catch (Exception e) {
            throw e; // 将异常继续抛出
        }
        return result;
    }

测试代码是否回滚,还是和前面一样的操作,就不赘述了. 代码的最终执行结果肯定是进行了回滚操作.

【代码示例】- 手动回滚事务

   @RequestMapping("/add2")
    @Transactional // 声明式事务
    public int add2(String username, String password) {
        // 非空校验
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return 0;
        }
        int result = userService.add(username, password, null);
        System.out.println("添加影响行数: " + result);
        try {
            int num = 10 / 0;
        } catch (Exception e) {
            // throw e;
            System.out.println("程序发生异常: " + e.getMessage());
            // 手动回滚事务 [得到当前事务并设置回滚] - 通过事务的切面拿到当前事务, 再设置回滚
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return result;
    }
手动回滚事务 : 通过事务的 AOP 拿到当前的事务, 然后设置回滚.

这种方式来处理事务的回滚, 显得更加优雅, 更推荐使用.

1.2.2 @Transactional 的工作原理

@Transactional 是基于 AOP实现的, AOP又是基于动态代理实现的 (JDK, CGLIB), 它在开始执行业务之前, 会通过代理实现开启事务, 在执行成功之后再提交事务, 如果中途出现了异常, 就会回滚事务.

@Transactional 实现思路

切面会拦截所有加了 @Transactional 注解的方法, 于是切点就有了, 然后开启事务与提交事务/回滚事务之间相当于是一个环绕通知.

2.事务隔离级别

在学 MySQL 的时候, 我们就已经知道了事务有四大特性: (ACID)

原子性 (Atomicity),
持久性(Consistency),
一致性 (Isolation) ,
隔离性 (Durability);

具体的概念在这篇博客中已经做过说明. - MySQL 事务的四大特性.

这四种特性中只有隔离性是可以设置的, 那么为什么要设置事务的隔离级别呢 ??

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

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

MySQL 中事务的隔离级别分为四种:

事务隔离级别

脏读

不可重复读

幻读

读未提交 (READ UNCOMMITTED)

读已提交 (READ COMMITTED)

×

可重复读 (REPEATABLE READ)

×

×

串行化 (SERIALIZABLE)

×

×

×

MySQL 默认事务的隔离级别 : 可重复读, 可通过命令 select @@global.tx_isolation,@@tx_isolation; 来进行查看.

1. 脏读 : 一个事务A,在执行的过程中,对数据进行了一系列修改,在提交到数据库之前(完成事务之前),另一个事务B,读取了对应的数据,此时这个B读到的数据都是一些临时的结果,后续可能马上就被A给改了,此时B的读取行为就是"脏读"!

2. 不可重复读 : 事务A提交了事务之后,事务B才开始读(读的时候加了锁),然后B在执行的过程中,A再次开启了事务, 修改了 B 读取的数据,此时B执行中,就导致两次读取操作结果可能就不一致!(侧重于修改)

3. 幻读 : 事务B读取过程中,事务A进行了更新操作 ( 新增/删除/修改),没有直接影响B正在读取的数据,但是影响到了B读取的结果集,事务B两次读取到的结果集不一样,这个就是幻读!幻读相当于不可重复读的特殊情况。(侧重于新增和删除)

2.1.1 Spring 中事务的隔离级别

Spring 中事务的隔离级别有五个, MySQL 中的四个加上 DEFAULT 级别;

Isolation.DEFAULT : 以连接的数据库的事务隔离级别为主.(以数据库的全局事务隔离级别为主)

在 Spring 中如何设置事务的隔离级别>>>

@RequestMapping("/add")
@Transactional(isolation = Isolation.DEFAULT)
public int add(String username, String password) {
    // 业务逻辑
}

3. Spring 事务的传播机制

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

在回答这个问题前, 先给大家举个例子 >>

1. 抛开事务的传播机制不说, 我们的业务就像 UU 跑腿一样, 你在你家附近买了一样东西, 然后让 UU跑腿的人送到你的手里, 如果送到了就没事 (commit), 如果没送到, 就可以找到对应跑腿的人 (或店家) 进行相应的赔偿 (rollback).
2. 可是实际生活中, 在网上买东西的人相对来说要多一些, 如果你在深圳, 但是在北京的一家网店上买了东西, 这时候就不可能叫 UU跑腿来送到你手里了. 那么快递一般都要经过很多个运输点, 这多个运输点对应着多批人, 如果你的快递在中途被弄丢了, 他们应该要怎样赔偿, 由谁来赔偿, 这就需要牵扯到了多个事务之间的传播机制了.

事务的隔离级别 : 解决的是多个事务同时调用数据库的问题!

而事务的传播机制解决的是一个事务在多个节点 (方法) 中传递问题!

上述例子将的就是上图中多个方法调用的时候, 发生异常应该要怎么去处理, 这就是传播机制的意义所在!

3.1 事务传播机制的级别

事务传播机制的级别分为 7 种:

1. Propagation.REQUIRED : 默认的事务传播级别, 它表示如果当前存在事务,则加入该事务; 如果当前没有事务, 则创建⼀个新的事务.
2. Propagation.SUPPORTS : 如果当前存在事务, 则加入该事务;如果当前没有事务,则以非事务的
方式继续运行.
3. Propagation.MANDATORY : (mandatory:强制性) 如果当前存在事务,则加入该事务; 如果当
前没有事务,则抛出异常.
4. Propagation.REQUIRES_NEW:表示创建一个新的事务, 如果当前存在事务, 则把当前事务挂
起. 也就是说不管外部方法是否开启事务, Propagation.REQUIRES_NEW 修饰的内部方法会新开
启自己的事务, 且开启的事务相互独立, 互不干扰.
5. Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起.
6. Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常.
7. Propagation.NESTED:如果当前存在事务,则 创建⼀个事务作为当前事务的嵌套事务来运行;如
果当前没有事务,则该取值等价于 Propagation.REQUIRED

这 7 中事务传播级别又可以分为三大类:

如果对于这三类事务传播机制, 不太理解的话, 下面把事务比做房子, 举一个生活中的例子 :

😁😁😁😁😁😁😁😁😁

  1. 支持当前事务 (普通伴侣)

  • REQUIRED (需要有) : 有房子就一起住, 没房子就一起赚钱买房子. (愿意陪你吃苦, 但一定要有房子)

  • SUPPORTS (可以有) : 有房子就一起住, 没房子就租房子住. (随缘的, 没房子也无所谓)

  • MANDATORY (强制有) : 有房子一起住, 没房子就分手. (不愿陪你吃苦)

  1. 不支持当前事务 (强势型伴侣)

  • REQUIRES_NEW : 不要你的房子, 咱们必须一起赚钱买房子. (看不上你的房子, 必须买新房子)

  • NOT_SUPPORTED : 不要你的房子, 咱们必须一起租房子. (不住你的房子, 必须租房子)

  • NEVER : 必须一起租房子, 你要有房子就分手. (看不上你的房子, 还得陪你环房贷)

  1. 嵌套事务 (懂事型伴侣)

  • NESTED : 有房子就以房子为根据地做点小生意, 赚钱了就继续发展, 赔钱至少还有房子; 如果没房子就一起赚钱买房子. (无风险创业, 保本懂事型伴侣)

对于上述3 类事务传播机制, 主要就是 REQUIRED (默认级别) 和 NESTED (嵌套事务) 不好区分>>

1. REQUIRED (默认级别) : 一荣俱荣, 一损俱损. 如果当前有事务, 执行过程中, 如果抛出异常, 那么就一起回滚, 如果否则一起提交.
2. NESTED (嵌套事务) : 如果当前有事务, 创建一个事务作为当前的嵌套事务来执行, 相当于在当前事务这里有一个保存点, 如果执行过程中嵌套事务抛出异常, 就回滚到保存点, 只回滚嵌套事务(局部回滚), 不会影响上一个方法中执行的结果.

【代码实现】 针对默认级别和嵌套事务的一个代码实现 >>

下面的代码针对 add 方法和 save 方法做了一个事务默认传播级别的测验, 两个方法都是添加方法, 如果途中没有抛异常, 数据库库就会新增两条数据, 否则一条也不新增.

Controller :

   @RequestMapping("/add2")
    @Transactional(propagation = Propagation.REQUIRED)
    public int add2(String username, String password) {
        // 非空校验
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return 0;
        }
        int result = userService.add(username, password, null);
        System.out.println("添加影响行数: " + result);

        int result2 = userService.save(username, password, null);
        System.out.println("添加影响行数: " + result2);
        
        return result;
    }

Service :

save 方法中有一个除 0 异常 >>

   @Transactional(propagation = Propagation.REQUIRED)
    public int add(String username, String password, String photo) {
        return userMapper.add(username, password, photo);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public int save(String username, String password, String photo) {
        try {
            int result = 10 / 0;
        } catch (Exception e) {
            System.out.println("ex: " + e.getMessage());
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return userMapper.add(username, password, photo);
    }

数据库当前信息 :

浏览器访问 add2 方法, 并传入参数, username=老六&password=123:

可以看到控制台打印了两次添加影响行数 : 1, 且出现一个除 0 异常, 那么是否真正插入到数据库中了, 需要查看一下数据库>>

发现并没有, 和上次查询的结果还是一样的. 所以符合我们的预期. (查看细致过程可以打断点进行调试)

【测试NESTED】

还是上述两个方法, 只不过把 Service 种的 save 方法的事务传播级别改为 NESTED.

浏览器访问 add2 方法, 并传入参数 usename=老六&passowrd=123

此时控制台依然打印了两次添加影响行数 : 1, 查询数据库验证插入情况 :

发现 add 方法的新增成功了, 而 save 方法的的新增回滚了, 也就是回滚到保存点, 这也符合我们的预期. (细致过程可以通过打断点的方式进行调试查看).


本篇文章就到这里了, 谢谢观看!!

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


文章名称:Spring事务和事务的传播机制-创新互联
链接地址:http://cdkjz.cn/article/jodsj.html
多年建站经验

多一份参考,总有益处

联系快上网,免费获得专属《策划方案》及报价

咨询相关问题或预约面谈,可以通过以下方式与我们联系

大客户专线   成都:13518219792   座机:028-86922220