Spring事务和事务传播机制(下)

        我们上一篇文章学习了 @Transactional 的基本使用。接下来我们学习 @Transactional 注解的使用细节。 @Transactional 注解当中有下面三个常见属性:

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

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

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

1. rollbackFor

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

        如下代码,我们把异常修改如下:

   @Transactional
    @RequestMapping("/r2")
    public String r2(String userName, String password) throws IOException {

        Integer result = userService.insertUser(userName,password);
        log.info("数据插入成功, result:"+result);
        if (true){
            throw new IOException();  //事务提交
        }

        return "注册成功";
    }

         浏览器访问http://127.0.0.1:8085/trans/r2?userName=wangyi&&password=111111,页面如下所示:

        数据库却显示王奕的信息添加成功;

        日志信息显示虽然发生异常了,但是信息却提交成功了;所以如果我们需要所有异常都回滚,需要来配置 @Transactional 注解当中的 rollbackFor属性,通过 rollbackFor 这个属性指定出现何种异常类型时,事务才会进行回滚

        在@transactional注解中添加rollbackfor属性,代码如下:

@Transactional(rollbackFor = {Exception.class})
    @RequestMapping("/r2")
    public String r2(String userName, String password) throws IOException {

        Integer result = userService.insertUser(userName,password);
        log.info("数据插入成功, result:"+result);
        if (true){
            throw new IOException();  //事务提交
        }

        return "注册成功";
    }

        浏览器访问:http://127.0.0.1:8085/trans/r2?userName=zhoushiyu&&password=111111,发现事务没有被提交而是被进行回滚了;即周诗雨的数据没有成功的提交到数据库中;

结论:

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

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

2. 事务的隔离级别

2.1 MySQL 事务隔离级别(回顾)

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

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

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

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

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

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

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

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

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

查看会话隔离级别(8.0以上版本):select @@transaction_isolation;

 

2.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 userNname,String password) throws IOException {
    return "r3";
}

3. Spring 事务传播机制

3.1 什么是事务传播机制

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

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

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

        类似于公司流程管理:

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

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

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

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

3.2 事务的传播机制有哪些

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 源码如下:

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.3 Spring 事务传播机制使用和各种场景演示

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

        1、REQUIRED(默认值)2、REQUIRES_NEW

3.3.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,所以它们三个共用同一个事务,也就是共存亡。

        运行程序,访问http://127.0.0.1:8085/user2/register?userName=zhangxin&password=111111,结果如下:

         我们观察发现,都插入成功了。

        我们修改为下面代码:

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共用一个事务,所以其他俩个也肯定会回滚。

浏览器访问http://127.0.0.1:8085/user2/register?userName=xuyangyuzhuo&password=111111,结果如下:

        数据库中两个表的信息页不会发生变化了,日志信息如下所示:

          关系图如下:

3.3.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是不管当前有没有事务,都会新建一个事务,而且新建的事务都是相互独立的。

        访问浏览器地址http://127.0.0.1:8085/user2/register?userName=feiqingyuan&password=111111,logService中依旧是是存在异常的,访问结果如下所示:

        日志信息如下所示:

        查看数据库的信息如下:

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

        关系图如下:

        代码逻辑图如上面图故事;

3.3.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是不支持有事务的,会抛出异常,运行程序,访问如下网址:http://127.0.0.1:8085/user2/register?userName=jiangshan&password=111111

 访问结果如下所示:

日志信息如下所示:

        查看数据库,数据并没有添加成功,如下所示:

 

3.3.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是一样的

        三个事务全部正常的情况下,运行程序,浏览器访问

http://127.0.0.1:8085/user2/register?userName=sunxiaoyan&password=111111,界面如下所示:

        当全部都成功时,是和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;
    }
}

        运行程序,浏览器访问http://127.0.0.1:8085/user2/register?userName=sushanshan&password=111111,结果页面如下:

        我们观察数据库,发现信息没有添加成功;说明两次插入的操作都进行回滚了。这里也能看出,如果部分失败,还是和REQUIRED一样的。

3.3.5 NESTED和REQUIRED有什么区别

        而和REQUIRED相比,NESTED可以部分回滚。当一个事务出现异常之后当前的事务进行回滚,但是相应的其他事务可以提交成功;故此NESTED可以实现了部分回滚。如果是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

ps:本文写到这里就结束了,感谢观看;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值