Spring事务和事务的传播机制(JavaEE进阶系列7)

目录

前言:

1.为什么需要事务

2.Spring中事务的实现

2.1编程式事务

2.2声明式事务

2.3@Transactional的作用范围

2.4@Transactional参数说明

2.5@Transactional的注意事项

2.6@Transactional工作原理

3.事务隔离级别

3.1事务特性的回顾

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

3.3MySQL事务隔离级别

3.4Spring的事务隔离级别

4.Spring事务的传播机制

4.1事务传播机制是什么

4.2为什么需要事务传播

结束语:


前言:

1.为什么需要事务

对于事务我们之前也是有所了解的我们在来回顾一下有关于事务的定义将一组操作封装成一个执行单元(封装在一起),要么全部执行成功,要么全部执行失败。

就拿转账来说:

  • 第一步操作:A账户向B账户转账100元,A账户 - 100元。
  • 第二步操作:B账户接收100元,B账户+100元。

如果没有事务,第一步如果成功了第二步失败了,那么A账户的100元就相当于是平白无故的人间蒸发了,不符合现实。所有使用事务就可以解决这个问题了,让这一组操作要么一起成功,要么一起失败。

2.Spring中事务的实现

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

  • 编程式事务(手动写代码的事务)。
  • 声明式事务(利用注解自动开启和提交事务)。

接下来我们先来回顾一下在MySQL中是如何使用的。

事务在MySQL中有三个重要的操作:开启事务(start transaction)、提交事务(commit)、回滚事务(rollback)

2.1编程式事务

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

  • 开启事务(获取事务)。
  • 提交事务。
  • 回滚事务。

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

java 复制代码
package com.example.demo.controller;

import com.example.demo.service.UserService;
import org.apache.catalina.User;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class UserController2 {
    @Resource
    private UserService userService;
    //JDBC事务管理器
    @Resource
    private DataSourceTransactionManager dataSourceTransactionManager;
    //定义事务属性
    @Resource
    private TransactionDefinition transactionDefinition;

    @RequestMapping("/save")
    public Object save(User user) {
        //开启事务
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
        //插入数据库
        int result = userService.save(user);
        //提交事务
        dataSourceTransactionManager.commit(transactionStatus);
        //回滚事务
        dataSourceTransactionManager.rollback(transactionStatus);
        return result;
    }
}

上述的实心方式太过于繁琐,所以接下来我们就使用声明式事务。

2.2声明式事务

声明式事务的实现很简单,只需要在需要的方法上添加@Transactional注解就可以实现了,无需手动开启事务和提交事务,进入方法时开启事务,方法执行完会自动提交事务,如果中途发生了没有处理的异常会自动回滚事务,具体代码实现如下所示:

model层代码:

java 复制代码
package com.example.demo.model;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class Userinfo {
    private int id;
    private String username;
    private String password;
    private String photo;
    private LocalDateTime createtime;
    private LocalDateTime updatetime;
    private int state;
}

dao层代码:

java 复制代码
package com.example.demo.dao;

import com.example.demo.model.Userinfo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    @Insert("insert into userinfo(username, password) values(#{username}, #{password})")
    int add(Userinfo userinfo);
}

service层代码:

java 复制代码
package com.example.demo.service;

import com.example.demo.dao.UserMapper;
import com.example.demo.model.Userinfo;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    public int add(Userinfo userinfo) {
        int result = userMapper.add(userinfo);
        System.out.println("add result -> " + result);
        return result;
    }
}

controller层代码:

java 复制代码
package com.example.demo.controller;

import com.example.demo.model.Userinfo;
import com.example.demo.service.UserService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

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

    @RequestMapping("/add")
    @Transactional
    public int add() {
        //1.非空判断
        Userinfo userinfo = new Userinfo();
        userinfo.setUsername("二二");
        userinfo.setPassword("123");
        //2.调用service执行添加
        int result = userService.add(userinfo);
        System.out.println("result: " + result);
        int n = 10 / 0;
        //3.将结果给前端
        return result;
    }
}

结果展示:

当启动服务器的时候访问页面的时候会出现报错异常。

在控制台中可以看到以及将数据添加到数据库中的信息。

但是在访问数据库的时候会发现没有数据,说明触发异常之后数据已经被回滚了。

2.3@Transactional的作用范围

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

  • **修饰方法时:**需要注意只能用到public方法上,否则不生效。(推荐此种方法)
  • **修饰类时:**表明该注解对该类中所有的public方法都生效。

2.4@Transactional参数说明

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

2.5@Transactional的注意事项

当程序发生异常,但被try-catch处理之后,就不会发生自动回滚事务了。

如下代码所示:

注意这里只是在上述代码的基础之上改动了controller层的代码。

结果展示:

浏览器不会显示异常。

控制台会输出添加的信息。

数据库会面会显示出添加之后的数据。

这里大家就好奇了,为什么明明是发生了异常,为什么事务不回滚了?

原因是因为在Spring中是使用代理来进行管理的,而当异常被捕获之后,外部的代理就没有办法感受到异常信息了,所以就会直接将数据添加到数据库中,不进行回滚事务了。

那么针对于上述的情况当然也会有解决方法,根据它的原因我们就可以找到解决问题的办法了,我们可以让代理感受到异常信息,这样事务就会发生回滚了。下面是被try-catch处理之后,不自动回滚事务的解决方案:

①将异常继续抛出去(代理对象就能感受到异常,也就能自动回滚事务了)

代码展示:

结果展示:

浏览器访问的时候仍然会报错。

控制台显示结果如下所示:

数据库再次查询数据的时候,发现三三这条数据没有被加载到数据库中,已经别回滚了。

②使用代码手动回滚事务。

代码展示:

结果展示:

这次在浏览器中展示的就不是一大堆的报错信息了。

接下来是控制台的信息展示,也是显示已经将数据添加到数据库中了。

在数据库中会发现数据不存在,就说明已经被回滚掉了。

注意:以上两种都可以用来解决被try-catch处理之后,不能自动回顾事务的问题,但是一般我们使用第二种解决方案多,也是推荐的一种写法。

2.6@Transactional工作原理

@Transactional是基于AOP实现的,AOP又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用JDK的动态代理,如果目标对象没有实现接口,会使用CGLIB动态代理。

@Transactional在开始执行业务之前,通过代理先开启事务,在执行之后再提交事务。如果中途遇到异常,则回滚事务。

3.事务隔离级别

3.1事务特性的回顾

事务有4大特性**(ACID),原子性、一致性、持久性、隔离性**。具体的概念如下所示:

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

在上述的四大特性中只有隔离性(隔离级别)是可以设置的。

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

设置事务的隔离级别是用来保障多个并发事务执行可控,更符合操作者的预期。

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

Spring中事务隔离级别可以通过@Transactional中的isolation 属性来进行设置,如下所示:

3.3MySQL事务隔离级别

MySQL的事务隔离级别有以下4种:

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

|----------------------------|--------|-----------|--------|
| 事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
| 读未提交(READ UNCOMMITTED) | | | |
| 读已提交(READ COMMITED) | × | | |
| 可重复读(REPEATABLE READ) | × | × | |
| 串行化(SERIALIZABLE) | × | × | × |

脏读、幻读、不可重复读导致的原因如下所示:

  • **脏读:**一个事务读取到另一个事务修改的数据之后,后一个事务又进行了回滚操作,从而导致第一个事务读取的数据是错误的。
  • **不可重复读:**一个事务两次查询得到的结果不同,因为在两次查询中间,有另一个事务把数据修改了。
  • **幻读:**一个事务两次查询中得到的结果集不同,因为在两次查询中另一个事务又新增了一部分数据。

在数据库中我们可以通过以下SQL语句来查询到当前全局事务隔离级别和当前连接的事务隔离级别:

那么在MySQL中到底有没有解决掉幻读的问题呢?

有但是又没彻底解决,因为 REPEATABLE READ 是通过 + MVCC 来进行解决幻读问题的,但是REPEATABLE READ又有两种读,一种是当前读 ,一种是快照读,快照读的时候是从内存中读取的。所以快照读通过+MVCC是可以解决掉幻读问题的,而当前读是读取当前的数据,可能会发生变化,所以当前读+MVCC是解决不掉幻读问题的,除非加锁。

所以要想彻底解决幻读问题有两种解决办法:

  1. 串行化。
  2. MVCC+锁。

3.4Spring的事务隔离级别

而Spring中事务隔离级别包含以下5种:

  • **Isolation.DEFAULT:**以连接的数据的事务隔离级别为主。
  • **Isolation.READ_UNCOMMITTED:**读未提交,可以读到未提交的事务,存在脏读。
  • Isolation.READ_COMMITTED: 读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重复读。
  • Isolation.REPEATABLE_READ: 可重复读,解决了不可重复读,但存在幻读(MySQL默认级
  • 别)。
  • **Isolation.SERIALIZABLE:**可以解决所有的并发问题,但是性能太低。

从上述介绍中可以看出,相比于MySQL的事务隔离级别,Spring的事务隔离级别只是多一个Isolation.DEFAULT(以数据库的全局事务隔离级别为主)。

我们只需要在Spring事务中设置@Transactional里的属性isolation属性的隔离级别即可,如下代码所示:

4.Spring事务的传播机制

4.1事务传播机制是什么

Spring事务传播机制定义了多个包含了事务的方法,互相调用,事务是如何在这些方法间进行传递的。他其实就是规定了多个事务在互相调用时,事务的执行行为。

4.2为什么需要事务传播

事务的隔离级别是保证多个并发执行的可控性(稳定性的),而事务传播机制是保证一个事务在多个调用方法间的可控性(稳定性)。就像是疫情期间,会存在不同的隔离方式,酒店隔离、居家隔离...,这就是为了保证疫情的可控性。事务的隔离级别解决的是多个事务同时调用一个数据库的问题,如下所示:

而事务的传播机制解决的是一个事务在多个节点(方法)中传递的问题,如下所示:

4.3事务的七大传播机制

Spring事务的传播机制包含以下7点:

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

针对于Propagation.REQUIRED它是表示如果当前存在事务,则加入该事务,没有存在则不加入,以非事务的方式继续运行。接下来我们通过画图和代码来给大家进行具体演示一下。

代码展示:

Controller层代码展示:

java 复制代码
package com.example.demo.controller;

import com.example.demo.model.Userinfo;
import com.example.demo.service.UserService;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

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

    @RequestMapping("/add")
    @Transactional(propagation = Propagation.REQUIRED)
    public int add() {
        //1.非空判断
        Userinfo userinfo = new Userinfo();
        userinfo.setUsername("思思");
        userinfo.setPassword("123456");
        //2.调用service执行添加
        int result = userService.add(userinfo);
        System.out.println("result: " + result);
        try {
            int n = 10 / 0;
        } catch (Exception e) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        //3.将结果给前端
        return result;
    }
}

Service层代码展示:

java 复制代码
package com.example.demo.service;

import com.example.demo.dao.UserMapper;
import com.example.demo.model.Userinfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public int add(Userinfo userinfo) {
        int result = userMapper.add(userinfo);
        System.out.println("add result -> " + result);
        insert(userinfo);
        return result;
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public int insert(Userinfo userinfo) {
        int result = userMapper.add(userinfo);
        System.out.println("add result -> " + result);
        int n = 10 / 0;
        return result;
    }
}

结果展示:

浏览器页面展示,会发现存在算数异常。

控制台会发现有插入的两条数据的记录。

在数据库中发现没有插入任何数据,说明在触发异常之后数据都被回滚了。

解析: 当使用Propagation.REQUIRED时说明第一个事务已经开启了事务了,则后续的事务发现前面的事务开启之后就会加入到事务中来。如下图所示:

针对于Propagation.SUPPORTS它是如存在当前事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

代码展示:

controller层代码展示:

java 复制代码
package com.example.demo.controller;

import com.example.demo.model.Userinfo;
import com.example.demo.service.UserService;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

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

    @RequestMapping("/add")
    @Transactional(propagation = Propagation.SUPPORTS)
    public int add() {
        //1.非空判断
        Userinfo userinfo = new Userinfo();
        userinfo.setUsername("思思");
        userinfo.setPassword("123456");
        //2.调用service执行添加
        int result = userService.add(userinfo);
        System.out.println("result: " + result);
        try {
            int n = 10 / 0;
        } catch (Exception e) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        //3.将结果给前端
        return result;
    }
}

service层代码展示:

java 复制代码
package com.example.demo.service;

import com.example.demo.dao.UserMapper;
import com.example.demo.model.Userinfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.SUPPORTS)
    public int add(Userinfo userinfo) {
        int result = userMapper.add(userinfo);
        System.out.println("add result -> " + result);
        insert(userinfo);
        return result;
    }

    @Transactional(propagation = Propagation.SUPPORTS)
    public int insert(Userinfo userinfo) {
        int result = userMapper.add(userinfo);
        System.out.println("add result -> " + result);
        int n = 10 / 0;
        return result;
    }
}

结果展示:

浏览器依然会报错:

控制台中会显示插入数据。

在数据库中国会发现插入了两条数据,说明他是以非事务的方式运行的。

画图解析:

针对于 Propagation.NESTED它表示当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,如果当前没有事务,则该取值等于PROPAGATION_REQUIRED。

代码展示:

Controller层代码展示:

java 复制代码
package com.example.demo.controller;

import com.example.demo.model.Userinfo;
import com.example.demo.service.UserService;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

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

    @RequestMapping("/add")
    @Transactional(propagation = Propagation.NESTED)
    public int add() {
        //1.非空判断
        Userinfo userinfo = new Userinfo();
        userinfo.setUsername("五五");
        userinfo.setPassword("123456");
        //2.调用service执行添加
        int result = userService.add(userinfo);
        System.out.println("result: " + result);

        userService.insert(userinfo);
//        try {
//            int n = 10 / 0;
//        } catch (Exception e) {
//            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
//        }
        //3.将结果给前端
        return result;
    }
}

service层代码展示:

java 复制代码
package com.example.demo.service;

import com.example.demo.dao.UserMapper;
import com.example.demo.model.Userinfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import javax.annotation.Resource;
@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public int add(Userinfo userinfo) {
        int result = userMapper.add(userinfo);
        System.out.println("add result -> " + result);
        return result;
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public int insert(Userinfo userinfo) {
        int result = userMapper.add(userinfo);
        System.out.println("add result -> " + result);
        try {
            int n = 10 / 0;
        } catch (Exception e) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return result;
    }
}

结果展示:

对于浏览器端仍然出现报错现象,但是不是算数异常错误了。

对于控制台依然会显示插入两条数据。

对于数据库而言它不会插入一条数据,所有的数据都会被回滚掉。

画图解释:

结束语:

好了这节小编就给大分享到这里啦,希望这节对大家有关于Spring事务和事务的传播机制的基础知识的了解有一定帮助,想要学习的同学记得关注小编和小编一起学习吧!如果文章中有任何错误也欢迎各位大佬及时为小编指点迷津(在此小编先谢过各位大佬啦!)

相关推荐
StayInLove17 分钟前
G1垃圾回收器日志详解
java·开发语言
对许21 分钟前
SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“
java·log4j
鹿屿二向箔22 分钟前
基于SSM(Spring + Spring MVC + MyBatis)框架的咖啡馆管理系统
spring·mvc·mybatis
无尽的大道25 分钟前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
小鑫记得努力34 分钟前
Java类和对象(下篇)
java
binishuaio38 分钟前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE40 分钟前
【Java SE】StringBuffer
java·开发语言
老友@40 分钟前
aspose如何获取PPT放映页“切换”的“持续时间”值
java·powerpoint·aspose
wrx繁星点点1 小时前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式
Upaaui1 小时前
Aop+自定义注解实现数据字典映射
java