Spirng中事务的传播性

本文详细解释了Spring中事务的传播规则,如REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER和NESTED,并通过代码示例展示了它们在数据库操作中的具体应用。例如,REQUIRED会加入现有事务或新建事务,而REQUIRES_NEW则始终创建新事务,即使外部已有事务。文章还讨论了可能出现的死锁问题以及如何避免它们。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在我们对数据库进行更改某些数据时, 我们往往需要用到事务来保持对数据操作的一致性和原子性等等,而Spring也为我们提供了开启事务的相关操作.

什么是事务的传播性?

事务的传播行为是为了解决业务层方法之间相互调用的事务问题, 当一个事务方法被另一个事务方法调用时, 事务该以哪种形态存在? 是开启一个新的事务? 还是放弃事务? 或者加入已经存在的事务?这些规则就涉及到了事务的传播性

Spring中事务的传播规则

  1. REQUIRED(默认规则): 如果当前存在事务, 则加入该事务。如果不存在事务, 则新建一个事务

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

  1. MANDATORY: 如果当前存在事务, 则加入事务。如果没有事务, 则抛出异常

  1. REQUIRES_NEW: 创建一个新的事务, 如果当前存在事务, 则把该事务挂起

  1. NOT_SUPPORTED: 以非事务方式运行, 如果当前存在事务, 则把当前事务挂起

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

  1. NESTED: 如果当前存在事务, 则创建一个事务作为当前事务的嵌套事务来运行;如果没有事务, 则自己创建一个新的事务

可能我们刚开始一听到上面的规则会感觉很迷惑, 那接下来我将会用代码的形式来展示上面的全部规则所代表的含义

代码演示:

我将在springboot中使用事务注解的方式进行演示, 我们创建一个bank表, 其中有小明和小吴的存款信息, 通过模拟转账的方式进行演示

数据也很简单, 表中只有名字(name)和存款(money)两个字段; 注意::在每一个演示完后我会将数据设置为下面的初始数据

然后我们在Mapper层创建两个函数来进行模拟转账的操作

// 更新小明的存款, 帮小明的存款加500
@Update("""
            update bank set money = money + 500 where name = '小明'
        """)
Integer updateMingMoney();

// 将小吴的存款减500
@Update("""
          update bank set money = money - 500 where name = '小吴'
         """)
Integer updateWuMoney();

然后我们在Service层进行操作,代码如下, 代码非常简单, 只有一个updateMoney函数来对小明的存款进行操作

    @Test
    @Transactional
    public void updateMoney() {
        // 将小明的存款进行加500的操作
        bankMapper.updateMingMoney();
    }

(1) REQUIRED演示:

由于spring中事务的默认传播规则就是REQUIRED, 所以不用指定Transactional注解的propagation属性即可.

我们首先在updateMoney方法中制造异常, 如下所示

    @Test
    @Transactional
    public void updateMoney() {
        // 将小明的存款进行加500的操作
        bankMapper.updateMingMoney();
        // 制造异常
        int i = 1 / 0;
    }

transferMoney方法体如下:

@Service
public class UpdateMoneyService {
    @Autowired
    private BankMapper bankMapper;

    @Autowired
    private TransferMoneyService transferMoneyService;

    @Transactional
    public void transferMoney() {
        // 将小吴的存款进行减500的操作
        bankMapper.updateWuMoney();
        // 再调用updateMoney的方法将小明的存款加500
        transferMoneyService.updateMoney();
    }
}

然后通过请求'hello'接口运行transferMoney方法:

@RestController
public class TransactionController {
    @Autowired
    UpdateMoneyService updateMoneyService;
    @RequestMapping("/hello")
    public Integer transferMoney() {
        updateMoneyService.transferMoney();
        return 1;
    }
}

结果肯定报错, 此时我们查看数据库的数据:

数据没有发生变动, 说明执行事务成功了;

此时我们再查看日志, 出现以下信息:

从上面可以知道, 当transferMoney方法已经存在事务时, spring就不会再为updateMoney方法创建一个新的事务, 而是使用同一个事务

接下来, 我们不再为transferMoney方法添加事务,再让我们看看数据是否还会保持正确

@Service
public class UpdateMoneyService {
    @Autowired
    private BankMapper bankMapper;

    @Autowired
    private TransferMoneyService transferMoneyService;

    // 不再添加事务
    //@Transactional
    public void transferMoney() {
        // 将小吴的存款进行减500的操作
        bankMapper.updateWuMoney();
        // 再调用updateMoney的方法将小明的存款加50
        transferMoneyService.updateMoney();
    }
}

可以查看数据库中的数据:

可以发现小吴的存款减少了500, 而小明的却没有, 说明对小明的操作回滚成功了, 而小吴却没有,

我们再查看控制台打印的信息:

可以发现spring为updateMoney创建了一个事务, 而没有为transferMoney创建事务(因为我们已经指定了对transferMoney不开启事务);所以导致updateMoney方法中对小明的操作进行了回滚, 而transferMoney中对小吴的操作没有进行回滚;

所以我们就知道了REQUIRED传播行为的特点: 有事务则加入事务, 没有事务则自己创建一个事务

接下来我们看REQUIRES_NEW:

(2)REQUIRES_NEW演示

REQUIRES_NEW则是无论调用者有没有事务, 它都会自己独立的创建一个事务, 与REQUIRED的区别就是REQUIRED当调用者有事务时会加入调用者的事务, 而REQUIRES_NEW则会创建一个新的事务;

当调用者开启了事务时, 使用了REQUIRES_NEW的被调用方法如果抛出了异常(没被捕获), 会造成数据的全部回滚

我们为updateMoney的事务注解指定传播的行为:

// 指定传播行为为REQUIRES_NEW
 @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateMoney() {
        // 将小明的存款进行加500的操作
        bankMapper.updateMingMoney();
        int i = 1 / 0;
    }

再运行(通过请求'/hello'接口)transferMoney(开启了事务)

  @Service
public class UpdateMoneyService {
    @Autowired
    private BankMapper bankMapper;

    @Autowired
    private TransferMoneyService transferMoneyService;

    @Transactional
    public void transferMoney() {
        // 将小吴的存款进行减500的操作
        bankMapper.updateWuMoney();
        // 再调用updateMoney的方法将小明的存款加500
        transferMoneyService.updateMoney();
    }
}

结果:

可以知道, 传播行为指定为REQUIRES_NEW时尽管有事务了, 自己依然还会创建事务(此处还会有注意事项,在最后将会提到). 这可能会造成以下的情况:

(1) 当transferMoney在调用完updateMoney后发生异常时, transferMoney中的其它对数据库的操作会进行回滚, 而updateMoney中的操作则不会进行回滚;

(2)updateMoney中发生异常, 但是异常在transferMoney中被捕获了而没有抛出异常, transferMoney中的操作则不会进行回滚

接下来, 我们将对上面的两种情况进行演示

第一种:

在transferMoney函数中制造异常, 而updateMoney则为正常

@Service
public class UpdateMoneyService {
    @Autowired
    private BankMapper bankMapper;

    @Autowired
    private TransferMoneyService transferMoneyService;

    @Transactional
    public void transferMoney() {
        // 将小吴的存款进行减500的操作
        bankMapper.updateWuMoney();
        // 再调用updateMoney的方法将小明的存款加500
        transferMoneyService.updateMoney();
        int i = 1 / 0;
    }
}
 
@Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateMoney() {
        // 将小明的存款进行加500的操作
        bankMapper.updateMingMoney();
    }

执行请求的结果:

可以发现小吴的money并没有变化, 而小明的money则增加了500, 说明出错后对小吴的操作进行了回滚, 而小明的则没有, 验证了上面的第一种情况, 再将数据复原

第二种:

在updateMoney函数中制造异常, 而在transferMoney函数中进行捕获而不抛出

@Service
public class UpdateMoneyService {
    @Autowired
    private BankMapper bankMapper;

    @Autowired
    private TransferMoneyService transferMoneyService;

    @Transactional
    public void transferMoney() {
        // 将小吴的存款进行减500的操作
        bankMapper.updateWuMoney();
        // 再调用updateMoney的方法将小明的存款加500
        try {
            transferMoneyService.updateMoney();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateMoney() {
        // 将小明的存款进行加500的操作
        bankMapper.updateMingMoney();
        // 制造异常
        int i = 1 / 0;
    }

请求接口后运行的结果:

可以看到小明的money没有变, 而小吴的money减少了500, 说明对小明的操作进行了回滚, 而对小吴的操作没有进行回滚, 验证了上面的第二种情况;

(3)SUPPORTS演示

由于SUPPORTS当没有事务时会以非事务的方式运行, 有事务时会加入事务(与REQUIRED一致),这里只演示当没有事务时的情景

updateMoney函数的代码:

@Service
public class TransferMoneyService {
    @Autowired
    private BankMapper bankMapper;

    @Transactional(propagation = Propagation.SUPPORTS)
    public void updateMoney() {
        // 将小明的存款进行加500的操作
        bankMapper.updateMingMoney();
        int i = 1 / 0;
    }
}

transferMoney函数的代码(取消掉了开启事务)

//    @Transactional  
public void transferMoney() {
        // 将小吴的存款进行减500的操作
        bankMapper.updateWuMoney();
        // 再调用updateMoney的方法将小明的存款加500
        transferMoneyService.updateMoney();
    }

请求'hello'接口后的结果:

小明与小吴的money都发生改变了, 说明updateMoney的事务并没有失效, 验证了当调用者没有事务时, 指定传播行为为SUPPORETS的被调用者并不会开启事务;

(4)MANDATORY

与SUPPORTS很相似, 只是当被调用者没有事务时, 会抛出错误, 这里只演示被调用者没有开启事务的情况;

将updateMoney的传播行为改为MANDATORY, 并且不刻意制造异常, 并且transfer不开启事务

 @Transactional(propagation = Propagation.MANDATORY)
    public void updateMoney() {
        // 将小明的存款进行加500的操作
        bankMapper.updateMingMoney();
    }

请求'/hello'接口的结果:

可以看到当调用者没有开启事务时, 使用了传播行为为MANDATORY的被调用者会抛出异常;

(5)NOT_SUPPORTED:

以非事务方式运行, 如果当前存在事务, 则把当前事务挂起, 无论调用者有没有开启事务, 使用了该传播行为的被调用者都不会开启事务

将updateMoney的事务传播行为改为NOT_SUPPORTED并制造异常, 再开启transferMoney的事务:

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void updateMoney() {
        // 将小明的存款进行加500的操作
        bankMapper.updateMingMoney();
        int i = 1 / 0;
    }
@Service
public class UpdateMoneyService {
    @Autowired
    private BankMapper bankMapper;

    @Autowired
    private TransferMoneyService transferMoneyService;

    @Transactional
    public void transferMoney() {
        // 将小吴的存款进行减500的操作
        bankMapper.updateWuMoney();
        // 再调用updateMoney的方法将小明的存款加500
        transferMoneyService.updateMoney();
    }
}

调用'hello'接口:

可以看到小明的money发生了变化, 而小吴没有, 说明对小吴的操作进行了回滚, 而对小明的操作则没有回滚, 查看控制台的信息可知transferMoney开启了事务, 而updateMoney则没有开启事务;

(6)NEVER

以非事务方式运行, 如果当前存在事务, 则抛出异常, 假如方法A调用了事务传播行为为NEVER的方法B, 如果A没有开启事务, 则B以没开启事务的方式正常执行;而如果A开启了事务,则会报错, 感兴趣的自己可以进行演示;

(7) NESTED

如果当前存在事务, 则创建一个事务作为当前事务的嵌套事务来运行;如果没有事务, 则自己创建一个新的事务, 这里与SUPPORTED几乎一致; 假如方法A调用了事务传播行为为NESTED的方法B, 如果方法B出现异常并且被A捕获, 则只会回滚方法B中的操作;如果方法B抛出了异常则会进行全部的回滚; 而如果方法A出现异常, 则会对全部的操作进行回滚;感兴趣的可以自己试试;

注意点:

(1): 如果我们在同一个类中使用没有开启事务的A方法调用开启了事务的B方法, 会造成B方法的事务失效, 这是由于在同一个类中方法之间的调用并没有经过AOP代理实现事务, 例如下面的调用, 会造成B方法的事务失效

@Service
public class UpdateMoneyService {  
  // 调用B方法, 会造成B方法的事务失效
    public void A() {
        B();
    }

    @Transactional
    public void B() {
        System.out.println("我是B方法");
        // 执行修改操作...
    }
}

解决办法: I.可以将两个方法分别放到两个类中;II.使用其代理类调用方法, 如下

@Service
public class UpdateMoneyService {  
     @Autowired
    private UpdateMoneyService updateMoneyService;

  // 调用B方法, 会造成B方法的事务失效
// 解决办法: 使用自己的代理类
    public void A() {
       updateMoneyService.B();
    }

    @Transactional
    public void B() {
        System.out.println("我是B方法");
        // 执行修改操作...
    }
}

(2)当我们的调用者开启了事务并且被调用者的传播行为为REQUIRES_NEW时,可能出现数据库死锁的情况;因为此时开启了两个事务, 当我们使用某个字段作为条件对数据库进行更新操作时(本文中的name字段), 如果条件字段没有索引, 则会造成死锁的现象, 导致数据库一直更新不成功, 这是由于当我们第一个事务进行更新时, 数据库会将该表锁起来, 而第二个事务也需要获得表锁进行更新, 由于第一个事务会等待第二个事务更新完毕后才会进行提交释放锁, 导致第二个事务始终拿不到锁, 导致第一个事务也不能成功, 从而造成死锁;

解决办法: 为条件字段添加合适的索引即可;

(3)可以在配置文件中添加logging.level.root=debug在控制台打印事务的创建过程

如果有讲的不好的地方, 请多多包涵!!谢谢!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值