Spring事务(2)——@Transaction详解

目录

一、rollbackFor

结论:

二、事务隔离级别

1、MySQL 事务隔离级别(回顾)

2、Spring 事务隔离级别

三、Spring 事务传播机制

1、什么是事务传播机制?

2、事务的传播机制有哪些?

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

(1)REQUIRED(加入事务)

(2)REQUIRES_NEW(新建事务)

(3)NEVER(不支持当前事务,抛异常)

(4)NESTED(嵌套事务)

 当全部都成功时

当部分失败

(5)NESTED和REQUIRED有什么区别


思维导图:

        gitee:https://gitee.com/cool_tao6/studying-java-ee-advanced/tree/master/spring-trans

        代码准备和上篇博客一样,地址:Spring事务(1)-CSDN博客

        通过上篇博客的学习,我们学习了 @Transactional 的基本使用。接下来我们学习 @Transactional 注解的使用细节。

        我们主要学习 @Transactional 注解当中的三个常见属性:

1、rollbackFor异常回滚属性。指定能够触发事务回滚的异常类型。可以指定多个异常类型

2、IsoIation事务的隔离级别。默认值为 IsoIation.DEFAULT

3、propagation事务的传播机制。默认值为:Propagation.REQUIRED


一、rollbackFor

        @Transactional 默认只在 遇到运行时异常Error 时才会回滚,非运行时异常不回滚(即Exception的子类中,除了 RuntimeException 及其子类,都不回滚)。继承关系图如下:

        之前为了演示事务回滚,手动设置了程序异常:

int a = 10 / 0;

        接下来我们把异常改为如下代码:

@Slf4j
@RequestMapping("/trans")
@RestController
public class TransactionalController {
    @Transactional
    @RequestMapping("/r2")
    public String r2(String name, String password) throws IOException {
        //用户注册
        Integer ret = userService.registry(name, password);
        log.info("用户数据插入成功");
        if(true) {
            throw new IOException();
        }
        if(ret > 0) return  "注册成功";
        return "注册失败";
    }
}

        把之前数据都删了,重新建表,现在表的元素如图:

        运行程序:127.0.0.1:8080/trans/r2?name="zhangsan"&&password="123",发现事务没有回滚,事务提交了。

        程序虽然发生异常了,但事务仍旧提交了

        如果我们需要所有异常都回滚,需要来配置 @Transactional 注解当中的 rollbackFor属性,通过 rollbackFor 这个属性指定出现何种异常类型时事务进行回滚。

@Slf4j
@RequestMapping("/trans")
@RestController
public class TransactionalController {
    @Transactional(rollbackFor = Exception.class)
    @RequestMapping("/r2")
    public String r2(String name, String password) throws IOException {
        //用户注册
        Integer ret = userService.registry(name, password);
        log.info("用户数据插入成功");
        if(true) {
            throw new IOException();
        }
        if(ret > 0) return  "注册成功";
        return "注册失败";
    }
}

        上面设置 rollbackFor = Exception.class,表示异常是Exception及其子类,都会进行事务回滚

        运行程序:运行程序:127.0.0.1:8080/trans/r2?name="zhangsan"&&password="123",发现事务回滚了。

结论:

1、在 Spring 的事务管理中,默认只在遇到运行时异常 RuntimeException 和 Error 时才会回滚

2、如果需要回滚指定类型的异常,可以通过 rollbackFor 属性来指定


二、事务隔离级别

1、MySQL 事务隔离级别(回顾)

        SQL 标准定义了四种隔离级别,MySQL全都支持。这四种隔离级别分别是:

1、读未提交(READ UNCOMMITTED)读未提交,也叫未提交读。该隔离级别的事务可以看到其他事务中未提交的数据

        因为其他事务未提交的数据可能会发生回滚,但是该隔离级别却可以读到,我们把该级别读到的数据称之为脏数据,这个问题称之为 脏读

2、读已提交(READ COMMITTED)读已提交,也叫提交读。该隔离级别的事务能读取到已经提交事务的数据

        该隔离级别不会有脏读的问题。但由于在事务的执行中可以读到其他事务提交的结果,所以在不同时间的相同 SQL 查询可能会得到不同的结果,这种现象叫作 不可重复读

3、可重复读(REPEATABLE READ)事务不会读到其他事务对已有数据的修改,即使其他事务已提交。也就可以确保同一事务多次查询的结果一致,但是其他事务新插入的数据,是可以感知到的。这也就引发了幻读问题。可重复读,是MySQL的默认事务隔离级别

        比如次级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这个现象叫作 幻读。

4、串行化(SERIALIZABLE)序列化,事务最高隔离级别。它会强制事务排序,使之不会发生冲突,从而解决了脏读、不可重复读、幻读的问题,但因为执行效率低,所以真正使用的场景并不多

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

select @@global.tx_isolation,@@tx_isolation;

        以上的SQL的执行结果如下:

2、Spring 事务隔离级别

        Spring 中事务隔离级别有 5 种

1、Isolation.DEFAULT以连接数据库的事务隔离级别为主

2、Isolation.READ_UNCOMMITTED读未提交,对应 SQL标准中 READ UNCOMMITTED

3、Isolation.READ_COMMITTED读已提交,对应 SQL标准中 READ COMMITTED

4、Isolation.REPEATABLE_READ可重复读,对应 SQL标准中 REPEATABLE READ

5、Isolation.SERIALIZABLE串行化,对应 SQL标准中 SERIALIZABLE

        源码如下:

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

        Spring 中事务隔离级别可以通过 @Transactional 中的 isolation 属性进行设置

@Transactional(isolation = Isolation.READ_COMMITTED)
@RequestMapping("/r3")
public String r3(String name,String password) throws IOException {
    //... 代码省略
    return "r3";
}

三、Spring 事务传播机制

1、什么是事务传播机制?

        事务传播机制就是:多个事务方法存在调用关系,事务是如何在这些方法间传播的

        比如有两个方法:A方法、B方法,它们都被 @Transaction 修饰,A方法 调用 B方法。A方法 运行时,会开启一个事务。当 A方法 调用 B方法 时,B方法本身就有事务了,此时 B方法 运行,它是加入 A方法 的事务?还是使用自己的事务?或者是自己新建一个事务呢?

        上述问题就涉及到了事务的传播机制了。

比如公司流程管理:

        执行任务之前,需要先写执行文档,任务执行结束,再写总结汇报,如图:

        此时 A部门 有一项工作,需要 B部门 的支援,此时 B部门 是直接使用 A部门的文档,还是新建一个文档呢?

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

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

2、事务的传播机制有哪些?

        @Transaction 注解支持事务传播机制的设置,通过 propagation 属性来指定传播行为。

        Spring 事务传播机制有以下7种:(方法A 调用 方法B,A -> B,类似 controller -> service

        1、Propagation.REQUIRED默认的传播机制。如果当前存在事务,则加入该事务;如果当前没有事务,则新建一个事务

如果 A 有事务,则用 A 的事务;如果 A 没有,则 B 新建一个事务

        4、Propagation.REQUIRES_NEW创建一个新的事务。如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会新开启自己的事务,且开启的事务相互独立

如果 A 有事务,则把 A 的事务挂起,B 新建一个事务

        7、Propagation.NESTED如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,如果当前没有事务,则该取值等价于 Propagation_REQUIRED

当存在事务,这里的事务当做车、房,嵌套事务就是把车、房抵押给银行,银行给我贷款,我用贷款的钱启动事业;如果生意失败,车、房也没了)。

        2、Propagation.SUPPORTS如果当前存在事务,则加入该事务。如果当前没有事务,则以非事务的方式继续运行

如果 A 有事务,B 就有 A 的事务;如果 A 没有事务,B 就以非事务的方式运行

        5、Propagation.NOT_SUPPORTED以非事务方式运行,如果当前存在事务,则把当前事务挂起(不用)

不管 A 有没有事务,B 都不用,以非事务方式运行

        3、Propagation.MANDATORY强制性。如果当前存在事务,则加入该事物。如果当前没有事务,则抛出异常

如果 A 有事务,B 就加入该事务;如果 A 没有事务,就抛出异常

        6、Propagation.NEVER以非事务方式运行,如果当前存在事务,则抛出异常

A 不能有事务,否则抛出异常

        源码如下:

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

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

        对于以上事务传播机制,我们重点关注一下两个:

1、REQUIRED(默认值)

2、REQUIRES_NEW

(1)REQUIRED(加入事务)

        看下面代码实现:

①用户注册,插入一条数据

②记录操作日志,插入一条数据(出现异常)

        观察 propagation = Propagation.REQUIRED 的执行结果

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

    @Transactional
    @RequestMapping("/register")
    public Boolean register(String userName, String password) {
        /**
         * 用户的插入和日志表的插入,应该在Service完成
         * 为了方便学习,放在Controller里完成
         */
        Integer result = userService.registry(userName, password);
        System.out.println("插入用户表,result" + result);
        //插入日志表
        Integer logResult = logService.insertLog(userName, "用户注册");
        System.out.println("插入用户表,logResult" + logResult);
        return true;
    }
}

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    @Transactional(propagation = Propagation.REQUIRED)
    public Integer registry(String name, String password) {
        return userInfoMapper.insert(name, password);
    }
}

@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public Integer insertLog(String name, String op) {
        //记录用户操作
        return logInfoMapper.insertLog(name, op);
    }
}

        现在的Controller、userService、logService各自都有事务,Controller使用的是默认的传播机制,也就是Propagation.REQUIRED,而userService和logService使用的也是Propagation.REQUIRED,所以它们三个共用同一个事务,也就是共存亡。

当程序没有错误时,启动程序

        URL:127.0.0.1:8080/u2/register?userName=lisi&password=123456

        都插入成功了。

现在修改LogService成错误的,代码如下:

@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public Integer insertLog(String name, String op) {
        //记录用户操作
        Integer result = logInfoMapper.insertLog(name, op);
        int a = 10 / 0;
        return result;
    }
}

        因为出错了,所以LogService肯定会回滚,因为Controller、userService、logService共用一个事务,所以其他俩个也肯定会回滚。

当有错误时,重新启动程序

         URL:127.0.0.1:8080/u2/register?userName=lisi&password=123456

        数据库也就不会新增数据了,如图:

        日志里也没有显示提交committing的字样。

        关系图如下:

(2)REQUIRES_NEW(新建事务)

        代码如下:

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

    @Transactional
    @RequestMapping("/register")
    public Boolean register(String userName, String password) {
        /**
         * 用户的插入和日志表的插入,应该在Service完成
         * 为了方便学习,放在Controller里完成
         */
        Integer result = userService.registry(userName, password);
        System.out.println("插入用户表,result" + result);
        //插入日志表
        Integer logResult = logService.insertLog(userName, "用户注册");
        System.out.println("插入用户表,logResult" + logResult);
        return true;
    }
}

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Integer registry(String name, String password) {
        return userInfoMapper.insert(name, password);
    }
}

@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Integer insertLog(String name, String op) {
        //记录用户操作
        Integer result = logInfoMapper.insertLog(name, op);
        int a = 10 / 0;
        return result;
    }
}

        REQUIRES_NEW是不管当前有没有事务,都会新建一个事务,而且新建的事务都是相互独立的。

现在运行程序,logService中是有错误的。

        URL:127.0.0.1:8080/u2/register?userName=wangwu&password=1234

        看看数据库里的内容

        可以看到logService发生错误后回滚了,但userService没有。

        关系图如下:

(3)NEVER(不支持当前事务,抛异常)

        代码如下:

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

    @Transactional
    @RequestMapping("/register")
    public Boolean register(String userName, String password) {
        /**
         * 用户的插入和日志表的插入,应该在Service完成
         * 为了方便学习,放在Controller里完成
         */
        Integer result = userService.registry(userName, password);
        System.out.println("插入用户表,result" + result);
        return true;
    }
}

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    @Transactional(propagation = Propagation.NEVER)
    public Integer registry(String name, String password) {
        return userInfoMapper.insert(name, password);
    }
}

        Controller有事务了,因为Propagation.NEVER是不支持有事务的,会抛出异常,运行程序

        URL:http://127.0.0.1:8080/u2/register?userName=zhaoliu&password=1234

        数据并没有添加成功

        关系图如下:

(4)NESTED(嵌套事务)

        代码如下:

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

    @Transactional
    @RequestMapping("/register")
    public Boolean register(String userName, String password) {
        /**
         * 用户的插入和日志表的插入,应该在Service完成
         * 为了方便学习,放在Controller里完成
         */
        Integer result = userService.registry(userName, password);
        System.out.println("插入用户表,result" + result);
        //插入日志表
        Integer logResult = logService.insertLog(userName, "用户注册");
        System.out.println("插入用户表,logResult" + logResult);
        return true;
    }
}

@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation = Propagation.NESTED)
    public Integer insertLog(String name, String op) {
        //记录用户操作
        Integer result = logInfoMapper.insertLog(name, op);
        //int a = 10 / 0;
        return result;
    }
}

@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    @Transactional(propagation = Propagation.NESTED)
    public Integer registry(String name, String password) {
        return userInfoMapper.insert(name, password);
    }
}

        这里只演示存在事务时,创建一个事务作为当前事务的嵌套事务来运行,如果不存在事务,就会创建一个,和REQUIRED是一样的。

        三个事务全部正常的情况下,运行程序,

URL:http://127.0.0.1:8080/u2/register?userName=zhaoliu&password=1234

 当全部都成功时

        是和REQUIRED是一样的,日志内容如下:

        数据库:

当部分失败

        其他不变,logService的代码如下:

@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation = Propagation.NESTED)
    public Integer insertLog(String name, String op) {
        //记录用户操作
        Integer result = logInfoMapper.insertLog(name, op);
        int a = 10 / 0;
        return result;
    }
}

        运行程序,URL:http://127.0.0.1:8080/u2/register?userName=zhouba&password=1234

        日志信息:

        数据库:

        都没有新增数据。

        说明两次插入的操作都进行回滚了。

        这里也能看出,如果部分失败,还是和REQUIRED一样的。

关系图如下:

(5)NESTED和REQUIRED有什么区别

        而和REQUIRED相比,NESTED可以部分回滚。

        其他部分和之前的一样,LogService代码如下:

@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    @Transactional(propagation = Propagation.NESTED)
    public Integer insertLog(String name, String op) {
        //记录用户操作
        Integer result = logInfoMapper.insertLog(name, op);
        try{
            int a = 10 / 0;
        }catch (Exception e) {
            //回滚当前事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return result;
    }
}

        运行程序,URL:127.0.0.1:8080/u2/register?userName=zhouba&password=1234

        数据库:

        可以看到只有logService回滚了,userService数据插入成功了,并没有回滚,实现了部分回滚的功能。

        如果是REQUIRED有部分回滚的话,那其余全部事务也会回滚。

        嵌套事务之所以能够实现部分事务的回滚,是因为事务中有一个保存点(savepoint)的概念,嵌套事务进入之后相当于新建了一个保存点,而回滚时只回滚到当前保存点。

        资料参考:MySQL :: MySQL 5.7 Reference Manual :: 13.3.4 SAVEPOINT, ROLLBACK TO SAVEPOINT, and RELEASE SAVEPOINT Statements

        REQUIRED是加入到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚,这就是嵌套事务和REUQIRED(加入事务的区别。)


四、总结

1、Spring中使用事务,有两种方式:编程式事务(手动操作)和声明式事务。其中声明式事务使用较多,在方法添加@Transactional 就可以实现了。

2、通过@Transactional(isolation = Isolation.SERIALIZABLE)设置事务的隔离级别。Spring中的事务给级别有5种。

3、通过@TransactionAl(propagation = Propagation.REQUIRED) 设置事务的传播机制,Spring中的事务传播机制有7种,重点关注REQUIRED(默认值)和REQUIRES_NEW。

评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tao滔不绝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值