在我们对数据库进行更改某些数据时, 我们往往需要用到事务来保持对数据操作的一致性和原子性等等,而Spring也为我们提供了开启事务的相关操作.
什么是事务的传播性?
事务的传播行为是为了解决业务层方法之间相互调用的事务问题, 当一个事务方法被另一个事务方法调用时, 事务该以哪种形态存在? 是开启一个新的事务? 还是放弃事务? 或者加入已经存在的事务?这些规则就涉及到了事务的传播性
Spring中事务的传播规则
REQUIRED(默认规则): 如果当前存在事务, 则加入该事务。如果不存在事务, 则新建一个事务
SUPPORTS: 如果当前存在事务, 则加入该事务。如果没有事务, 则以非事务的方式运行
MANDATORY: 如果当前存在事务, 则加入事务。如果没有事务, 则抛出异常
REQUIRES_NEW: 创建一个新的事务, 如果当前存在事务, 则把该事务挂起
NOT_SUPPORTED: 以非事务方式运行, 如果当前存在事务, 则把当前事务挂起
NEVER: 以非事务方式运行, 如果当前存在事务, 则抛出异常
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在控制台打印事务的创建过程
如果有讲的不好的地方, 请多多包涵!!谢谢!!