Spring事务管理和事务传播机制详解


目录

一.简单理解事务

二.Spring中事务的实现

编程式事务

声明式事务

三.@Transactional 详解

▐ 异常回滚属性 rollbackFor

▐ 事务隔离级别 Isolation

▐ 事务的传播机制 propagation


一.简单理解事务

事务是⼀组操作的集合,是⼀个不可分割的操作。

事务会把一组操作作为一个整体,这组中的全部操作要么同时成功,要么同时失败,不允许一部分成功一部分失败的情况。就拿现实生活中大家玩游戏来举例:

游戏里面有许多充值道具,诸如王者荣耀里面英雄的皮肤,瓦洛兰特里面枪械的皮肤,对于这部分游戏内容我们需要充钱才能获得,在充钱的过程中不知道大家有没有担心过这样一件事:“万一充钱的时候突然网卡了,我钱花了但是没有给我点券和皮肤,把我钱吞了怎么办?”。但是这么多年来这个担忧却从来没有发生过,即使是网络质量不佳我们也只会出现以下俩种情况:

  1. 玩家进行充值后得到了应有的点券或皮肤
  2. 因为网络卡顿,导致充值失败,玩家既没有花费金钱也没有得到游戏道具

那么在这么一个充皮肤的例子中就有俩个操作:①玩家通过微信支付向游戏厂商进行充值操作 ②游戏厂商将皮肤或者点券发放给玩家。将上述俩种结果的情况转化为逻辑关系则:

  • ①成功,并且②也成功
  • ①失败,并且②也失败

作为玩家,我们不希望充值后没有得到应有的奖励(①成功,但②失败);作为游戏厂商,我们不希望玩家不消费就获得充值道具(①失败,但②成功)。对于这样充值皮肤的过程,我们就可以将其理解成为一个事务,要么①成功,并且②也成功;要么①失败,并且②也失败,总之俩个操作必须同时成功或者同时失败。

事务的应用遍布在生活中的各处,诸如银行转账、秒杀下单...

作为开发者,通过事务的处理来保证数据的一致性以及用户的体验是必不可少的,在MySQL中我们也常常用到事务的处理

-- 开启事务
start transaction;
-- 提交事务
commit;
-- 回滚事务
rollback;

那么在Spring中如何使用事务呢?

二.Spring中事务的实现

Spring中对于事务也进行了实现,Spring中的事务操作分为俩类:

  • 编程式事务:手动写代码操作事务
  • 声明式事务:利用注解自动开启和提交事务

编程式事务

Spring⼿动操作事务和MySQL 操作事务类似,我们可以通过以下几个操作来完成

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

SpringBoot中内置了两个对象用来创建事务:DataSourceTransactionManager 和 TransactionDefinition

  • DataSourceTransactionManager 事务管理器,⽤来获取事务(开启事务),提交或回滚事务
  • TransactionDefinition 是事务的属性,在获取事务的时候需要将 TransactionDefinition 传递进去从而获得⼀个事务 TransactionStatus

通过依赖诸如的方式将上述俩个对象注入进来后,通过事务管理器对象dataSourceTransactionManagergetTransaction()方法并且将事务属性对象transactionDefinition作为参数传入就可以获取到一个事务对象,即默认此时开启了事务,当我们需要提交或回滚事务的时候就可以通过事务管理器对象的commit()roolback()方法实现,代码如下:

@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@Autowired
private UserService userService;

public String registry(String name,String password){
    // 开启事务
    TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
    //业务逻辑 用户注册
    userService.registryUser(name,password);
    //提交事务
    dataSourceTransactionManager.commit(transactionStatus);
    //回滚事务
    //dataSourceTransactionManager.rollback(transactionStatus);
    return "注册成功";
}

以上是我们手动设置进行提交和回滚的方式,Spring另外还提供了通过注解来实现事务的方式,即声明式事务

声明式事务

声明式事务的实现很简单,只需要在需要事务的⽅法上添加 @Transactional 注解就可以实现了,⽆需⼿动开启事务和提交事务,进入方法时⾃动开启事务,⽅法执⾏完成会⾃动提交事务,如果中途发⽣了没有处理的异常则会⾃动回滚事务。如下所示:

@Autowired
private UserService userService;

@Transactional
public String registry(String name,String password){
    //⽤⼾注册
    userService.registryUser(name,password);
    return "注册成功";
}

如果抛出了异常,那么整个方法就会自动回滚

@Autowired
private UserService userService;

@Transactional
public String registry(String name,String password){
    //⽤⼾注册
    userService.registryUser(name,password);
    log.info("⽤⼾数据插⼊成功");
    //强制程序抛出异常
    int a = 10/0;
    return "注册成功";
}

@Transactional 可以⽤来修饰⽅法或类,

  • 修饰⽅法时:只有修饰 public ⽅法时才⽣效(修饰其他⽅法时不会报错,但也不会⽣效)
  • 修饰类时:对 @Transactional 修饰的类中所有的 public ⽅法都⽣效

⽅法/类被 @Transactional 注解修饰时,在⽬标⽅法执⾏开始之前会⾃动开启事务,⽅法执⾏结束之后⾃动提交事务,如果在⽅法执⾏过程中出现异常且异常未被捕获,就进⾏事务回滚操作,如果异常被程序捕获,⽅法就被认为是成功执⾏,依然会提交事务。如下通过try...catch...将异常捕获并弹出异常信息。

@Autowired
private UserService userService;

@Transactional
public String registry(String name,String password){
    //⽤⼾注册
    userService.registryUser(name,password);
    log.info("⽤⼾数据插⼊成功");
     //对异常进⾏捕获
     try {
         //强制程序抛出异常
         int a = 10/0;
     }catch (Exception e){
         e.printStackTrace();
     }
    return "注册成功";
}

运⾏程序会发现虽然程序出错了,但是由于异常被捕获并且只是弹出了异常信息,并没有通过throw抛出该异常,所以事务依然得到了提交

反之如果想要实现事务的回滚,通过throw抛出异常即可

@Autowired
private UserService userService;

@Transactional
public String registry(String name,String password){
    //⽤⼾注册
    userService.registryUser(name,password);
    log.info("⽤⼾数据插⼊成功");
     //对异常进⾏捕获
     try {
         //强制程序抛出异常
         int a = 10/0;
     }catch (Exception e){
         //将异常重新抛出去
         throw e;
     }
    return "注册成功";
}

另外也可以自己手动设置回滚事务,使⽤ TransactionAspectSupport.currentTransactionStatus() 得到当前的事务,并使⽤ setRollbackOnly 设置 setRollbackOnly

@Autowired
private UserService userService;

@Transactional
public String registry(String name,String password){
    //⽤⼾注册
    userService.registryUser(name,password);
    log.info("⽤⼾数据插⼊成功");
     //对异常进⾏捕获
     try {
         //强制程序抛出异常
         int a = 10/0;
     }catch (Exception e){
         // ⼿动回滚事务
         TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
     }
    return "注册成功";
}

三.@Transactional 详解

@Transactional 注解中有以下三个常⻅属性:

  1. rollbackFor: 异常回滚属性,指定能够触发事务回滚的异常类型,可以指定多个异常类型
  2. Isolation: 事务的隔离级别,默认值为 Isolation.DEFAULT
  3. propagation: 事务的传播机制,默认值为 Propagation.REQUIRED

▐ 异常回滚属性 rollbackFor

@Transactional默认只在遇到运行时异常Error时才会回滚,⾮运⾏时异常不回滚。即下图中蓝色部分的内容才会回滚。

如下,我们刻意抛出IOException,但是由于IOException不是运行时异常,也不是Error,则该事务仍然会正常提交事务,并不会进行回滚操作

@Autowired
private UserService userService;

@Transactional
public String registry(String name,String password) throws IOException {
    //⽤⼾注册
    userService.registryUser(name,password);
    log.info("⽤⼾数据插⼊成功");
    if(true) {
        throw new IOException();
    }
    return "注册成功";
}

如果我们希望其他的异常也会引起事务的回滚,那么通过rollbackFor属性来设置即可,如下就是对于所有异常的抛出都会引起事务的回滚。一旦我们按照下面这样编码,那么即使是抛出IO异常也会引起事务的回滚,因为IOException是继承于Exception的子类

@Autowired
private UserService userService;

@Transactional(rollbackFor = Exception.class)
public String registry(String name,String password) throws IOException {
    //⽤⼾注册
    userService.registryUser(name,password);
    log.info("⽤⼾数据插⼊成功");
    if(true) {
        throw new IOException();
    }
    return "注册成功";
}

 

▐ 事务隔离级别 Isolation

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

首先回顾一下SQL中的事务隔离级别,SQL 标准定义了四种隔离级别,且MySQL 全都⽀持,这四种隔离级别分别是:

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

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

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

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

3.可重复读(REPEATABLE READ):事务不会读到其他事务对已有数据的修改,即使其他事务已提交,也可以确保同⼀事务多次查询的结果⼀致,并且可重复读是 MySQL 的默认事务隔离级别。

隐患:由于其他事务新插⼊的数据也是可以感知到的,这也就引发了幻读问题,⽐如此级别的事务正在执⾏时,另⼀个事务成功的插⼊了某条数据,但因为它每次查询的结果都是⼀样的,所以会导致查询不到这条数据,⾃⼰重复插⼊时⼜失败(因为唯⼀约束的原因),明明在事务中查询不到这条信息,但⾃⼰就是插⼊不进去,这个现象叫幻读

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

Spring中事务隔离级别有5种,相较于上述的SQL的事务隔离级别,多了一个DEFAULT级别,该级别表示当前连接的数据库是什么就用该数据库的事务隔离级别;比如我们连上MySQL,由于MySQL的默认级别是可重复读,那么在我们不做任何修改的情况下,Spring中的事务隔离级别就是可重复读。

  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)

▐ 事务的传播机制 propagation

事务传播机制:多个事务⽅法存在调⽤关系时,控制事务如何在这些⽅法间进⾏传播的机制

⽐如有两个⽅法A、B都被 @Transactional 修饰,且A⽅法调⽤B⽅法。A⽅法运⾏时会开启⼀个事务,当A调⽤B时,B⽅法本⾝也有事务,当B⽅法运⾏时是加⼊A的事务,还是创建⼀个新的事务呢?

这就涉及到了事务的传播机制,事务隔离级别解决的是多个事务同时调⽤⼀个数据库的问题,而事务传播机制解决的是⼀个事务在多个⽅法中传递的问题

@Transactional 注解⽀持事务传播机制的设置,通过 propagation 属性来指定传播⾏为

Spring 事务传播机制有以下 7 种:

  1. Propagation.REQUIRED:默认的事务传播级别。如果当前存在事务,则加⼊该事务;如果当前没有事务,则创建⼀个新的事务。
  2. Propagation.SUPPORTS:如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的⽅式继续运⾏。
  3. Propagation.MANDATORY:如果当前存在事务,则加⼊该事务;如果当前没有事务, 则抛出异常(强制性)
  4. Propagation.REQUIRES_NEW:不管当前有没有事务都要创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部⽅法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部⽅法都会新开启⾃⼰的事务,且开启的事务相互独⽴,互不⼲扰。
  5. Propagation.NOT_SUPPORTED:以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起(不⽤)
  6. Propagation.NEVER:以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。
  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;
    }
}

上文中的描述可能有些枯燥,我们可以用一对新人买房的实况来理解事务传播机制:

  • Propagation.REQUIRED : 需要有房⼦,如果你有房,我们就⼀起住;如果你没房,我们就⼀起买房。(如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则创建⼀个新的事务)
  • Propagation.SUPPORTS : 可以有房⼦,如果你有房,那就⼀起住;如果没房,那就租房. (如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则以⾮事务的⽅式继续运⾏)
  • Propagation.MANDATORY : 如果你有房,我们就⼀起住;如果没房就离婚 (如果当前存在事务, 则加⼊该事务. 如果当前没有事务, 则抛出异常)
  • Propagation.REQUIRES_NEW : 必须买新房,不管你有没有房,必须要两个⼈⼀起买房,即使有房也不住。 (创建⼀个新的事务. 如果当前存在事务, 则把当前事务挂起)
  • Propagation.NOT_SUPPORTED : 不需要房,不管你有没有房,我都不住,必须租房。(以⾮事务⽅式运⾏,如果当前存在事务, 则把当前事务挂起)
  • Propagation.NEVER : 不能有房⼦。 (以⾮事务⽅式运⾏, 如果当前存在事务, 则抛出异常)
  • Propagation.NESTED : 如果你没房,就⼀起买房;如果你有房,就在这个房子里面再修一个养宠物的小房子。(如果如果当前存在事务, 则创建⼀个事务作为当前事务的嵌套事务来运⾏. 如果当前没有事务, 则该取值等价于 PROPAGATION_REQUIRED )



 本次的分享就到此为止了,希望我的分享能给您带来帮助,创作不易也欢迎大家三连支持,你们的点赞就是博主更新最大的动力!如有不同意见,欢迎评论区积极讨论交流,让我们一起学习进步!有相关问题也可以私信博主,评论区和私信都会认真查看的,我们下次再见

  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

luming.02

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

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

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

打赏作者

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

抵扣说明:

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

余额充值