spring事务传播行为

前言:在Spring中,我们可以通过声明式事务,实现对数据库操作的事务管理。其中,在声明式事务时,有一个事务的属性为propagation,即事务的传播行为。今天,就来讨论该属性的作用。

什么是事务的传播行为?
首先,事务的传播行为,可以拆成两部分理解,即事务的传播,和事务的行为。指的是,当有两个或以上的方法同时声明为事务方法(事务方法:即加了事务管理的增删改方法)时,如果在一次程序执行过程中,这些事务方法彼此间相互调用,那么这些事务方法的事务,应该如何来进行管理?
其中,事务的传播,指的是嵌套调用的多个事务方法,是否会共享同一个事务,即调用者所处的事务是否会传播给被调用者(前提:两者都是属于事务方法)。
而事务的行为,主要指的就是事务的提交或者回滚。

   OK,那么解析完事务的传播行为的概念,接下来再说一个,关于事务,核心关键点只有一个,那就是,一个事务,就是一条数据库连接。而刚刚提到的事务的传播,即为调用者和被调用者共享同一个事务,也就是共用同一个数据库连接。

   所以,事务的传播行为,通俗点理解,就是:

传播行为,就是设置当前这个事务方法(被调用者),是不是和调用者所在的大事务(外层事务),共享同一个事务(即是用同一条连接)。

事务的传播行为有哪些?
说完事务的传播行为的概念,接下来,我们对它所能设置的值进行解析,即事务有哪些传播行为。

   Spring中定义了七种事务的传播行为,完整版的传播行为如下:

   其中,REQUIRED(事务传播行为的默认值)、REQUIRED_NEW这两个是最重要也是最常用的事务传播行为。所以接下来,我们就针对这两个进行举例说明。

两种事务传播行为的举例说明
用最经典的银行转账例子,来进行这一次的举例。

   首先,准备两张表,分别用来表示账户持有人、账户余额:

账户持有人

CREATE TABLE t_account(
id INT PRIMARY KEY AUTO_INCREMENT,
t_name VARCHAR(20)
);

账户余额

CREATE TABLE t_balance(
t_id INT,
t_money DECIMAL,
FOREIGN KEY(t_id) REFERENCES t_account(id)
);

1
2
3
4
5
6
7
8
9
10
11
12
13
其中的账户余额表,就是我们这一次要操作的对象。

   接着,在java层面,分别创建BalanceDao及其实现类、BalanceService业务类,用来操作t_balance表中的数据。

// Dao接口: 为了实验效果明显,将账户余额增加钱和减少钱两个操作分成两个方法:
public interface BalanaceDao {

//增加余额
int addBalance(Integer id, BigDecimal money);

//减少余额
int reduceBalance(Integer id, BigDecimal money);

}
1
2
3
4
5
6
7
8
9
10
// Dao实现类:
@Repository
public class BalanceDaoImpl implements BalanaceDao {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@Transactional(propagation = Propagation.REQUIRED)
public int addBalance(Integer id, BigDecimal money) {
    String sql = "update t_balance set t_money = t_money + ? where t_id = ? ;";

    //模拟异常:

// int i = 1 / 0;

    return jdbcTemplate.update(sql, money, id);
}

@Override
@Transactional(propagation = Propagation.REQUIRED)
public int reduceBalance(Integer id, BigDecimal money) {
    String sql = "update t_balance set t_money = t_money - ? where t_id = ? ;";

    //模拟异常:

// int i = 1 / 0;

    return jdbcTemplate.update(sql, money, id);
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Service层:
@Service
public class BalanceService {

@Autowired
private BalanaceDao balanaceDao;

//转账业务:用户1给用户2转账100元
@Transactional
public void transfer(){
    //用户1 减少 100元
    balanaceDao.reduceBalance(1, new BigDecimal(100));

    //模拟异常:

// int i = 1/0;

    //用户2 增加 100元
    balanaceDao.addBalance(2, new BigDecimal(100));

    //模拟异常:

// int j = 1/0;
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
对Dao层和Service层进行说明:首先,这一次是通过Service层的transfer() 来调用Dao层的 reduceBalance() 和 addBalance() 进行实验。 这三个方法都声明为事务方法,都加了@Transactional 注解。

   所以事务的传播行为,指的就是:当 transfer() 为事务方法,已经开启了自己的事务时(拥有一条数据库连接),如果调用了另外的两个事务方法reduceBalance() 和 addBalance() , 这两个事务方法是会独立开启事务(获取属于自己的一条独立数据库连接), 还是和调用者即 transfer() 共享同一个事务(和调用者共用同一条数据库连接 )。

先来第一种事务传播行为:REQUIRED
REQUIRED 是Spring中事务的默认传播行为,当方法被声明为事务方法,且传播行为设置为 REQUIRED 时,即表示 这个方法必须运行在事务中,但是否是自己的独立事务并无关紧要。

   类比一个不恰当的例子,老王约我去旅游,我必须是坐车去旅游。 当 我的事务为 REQUIRED 时,表示 如果老王开了车,那我就坐老王的车;如果老王没开车,那我就自己开车去。

   那么这种情况下,这个设置为REQUIRED的事务方法,就和调用者同舟共济,要么一起提交,要么一起回滚。

开始实验:
初始时,表中数据如下:

   测试类:

@Test
public void test(){
// 创建SpringIOC容器
ApplicationContext app = new ClassPathXmlApplicationContext(“TxXml.xml”);

    BalanceService balanceService = app.getBean("balanceService", BalanceService.class);

    //测试转账业务
    balanceService.transfer();
}

1
2
3
4
5
6
7
8
9
10
实验一:调用者的事务出现了异常,被调用者正常运行
首先,在Service中开启模拟异常
@Transactional
public void transfer(){
//用户1 减少 100元
balanaceDao.reduceBalance(1, new BigDecimal(100));

    //用户2 增加 100元
    balanaceDao.addBalance(2, new BigDecimal(100));

    //模拟异常:
    int j = 1/0;
}

1
2
3
4
5
6
7
8
9
10
11
Dao层的两个方法保持不变,两个方法的事务传播行为属性都设置为REQUIRED (默认值) 。
运行结果如下:

IDEA报算术异常

而数据库中的数据并未更改

说明:在Service的transfer() 中,虽然两个事务方法都成功执行了,但由于最后出现了运行时异常,所以导致transfer()的事务发生了回滚。而Dao层的两个事务方法由于传播行为设置为REQUIRED,所以和transfer() 的事务同舟共济,就一起被回滚了,因此数据库中的数据并未发生更改。

实验二:调用者正常运行,但被调用者中出现了异常
Service中关闭模拟异常,而在Dao中的addBalance() 开启模拟异常
//转账业务:用户1给用户2转账100元
@Transactional
public void transfer(){
//用户1 减少 100元
balanaceDao.reduceBalance(1, new BigDecimal(100));

    //用户2 增加 100元
    balanaceDao.addBalance(2, new BigDecimal(100));

}

1
2
3
4
5
6
7
8
9
10
@Override
@Transactional(propagation = Propagation.REQUIRED)
public int addBalance(Integer id, BigDecimal money) {
String sql = “update t_balance set t_money = t_money + ? where t_id = ? ;”;

    //模拟异常:
    int i = 1 / 0;

    return jdbcTemplate.update(sql, money, id);
}

1
2
3
4
5
6
7
8
9
10
运行,查看结果:
IDEA中仍然报错:
数据库中的表仍然没有发生更改:
说明:虽然Service中的transfer() 并未发生异常,reduceBalance() 也正常运行,但由于addBalance() 中出现异常,而异常没有被处理,会向上传导,所以异常会传导到调用者处,也就是transfer()中,所以它的事务会进行回滚。而这三个事务方法共享同一个事务,因此,最终reduceBalance() 的操作并未生效。

实验三:设调用者为非事务方法,且调用者的方法最后出现异常,两个被调用方法仍然是事务方法,且无异常:
Service层代码:
//转账业务:用户1给用户2转账100元
// @Transactional 取消事务注解
public void transfer(){
//用户1 减少 100元
balanaceDao.reduceBalance(1, new BigDecimal(100));
//用户2 增加 100元
balanaceDao.addBalance(2, new BigDecimal(100));
//模拟异常:
int j = 1/0;
}
1
2
3
4
5
6
7
8
9
10
Dao层的代码无异常,且都为事务方法
查看运行结果,如下:
IDEA仍报异常:

但与前两个实验不同的是,数据库中的数据发生了更改。

说明:由于Service层的transfer()不是事务方法,因此运行时并未开启事务,但调用第一个Dao层方法即reduceBalance()时,reduceBalance()是事务方法,所以会自己开启一个事务,顺利执行完后就提交然后关闭该事务;然后接着调用addBalance(),它也是个事务方法,由于调用者自身没有开启事务,所以addBalance()也会自己开启一个事务,执行完自己的数据库操作后,提交事务然后关闭。最后,当程序的调用权又回到了transfer()中时,虽然最后出现了运行时异常,但由于前面的两个Dao层的方法已经被成功调用并且各自开启事务提交了修改数据,因此最后出现的异常并没有影响到数据库的修改。

总结:
当事务的属性 propagation 为 REQUIRED时,该事务方法必须运行在事务中,若调用者(调用了该事务方法的方法)本身已开启了事务(即已经获取了数据库的连接),那么该事务方法会获取到调用者的事务(即同一条数据库连接),并在该事务中执行自己的数据库操作;若调用者自身不是事务方法,没有开启事务,那么该事务方法会自己获取一条数据库连接以开启事务,并在执行完自己的数据库操作后关闭该连接。

   总而言之,REQUIRED 的传播行为,表示了这嵌套调用的多个事务方法,是共用同一个事务,同舟共济,要么一起提交,要么一起回滚。

画个示意图:

再说第二种事务传播行为:REQUIRES_NEW
REQUIRES_NEW 的事务传播行为,是指当前事务方法,必须是自己新开一个事务执行数据库操作(即获取一条新的数据库连接),不与调用者共用同一个事务,即使调用者本身就处在一个事务中。如果调用者本身就处于一个事务中,那么当前事务方法执行时,调用者所在的事务将被挂起,而等当前事务方法执行完毕,将自己的事务关闭后,调用者的事务才得以继续往下执行。

   还是举个不恰当的例子:隔离老王约我(设置为REQUIRES_NEW的事务方法)去旅游,到目的地的方式有多种,其中一种方式是自己开车(相当于自己开启了事务),那么无论老王是否有开车(无论调用者是否有开启自己的事务),我都要自己开车(设置为REQUIRES_NEW的事务方法都要开启自己的事务)。

   这种情况下,设置为REQUIRES_NEW的事务方法会和调用者的事务分道扬镳。

开始实验:
表中的数据还原:

   仍然采用上面的Service层和Dao层:

@Service
public class BalanceService {

@Autowired
private BalanaceDao balanaceDao;

//转账业务:用户1给用户2转账100元
@Transactional
public void transfer(){
    //用户1 减少 100元
    balanaceDao.reduceBalance(1, new BigDecimal(100));

    //模拟异常:

// int i = 1/0;

    //用户2 增加 100元
    balanaceDao.addBalance(2, new BigDecimal(100));

    //模拟异常:

// int j = 1/0;
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Repository
public class BalanceDaoImpl implements BalanaceDao {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int addBalance(Integer id, BigDecimal money) {
    String sql = "update t_balance set t_money = t_money + ? where t_id = ? ;";

    //模拟异常:

// int i = 1 / 0;

    return jdbcTemplate.update(sql, money, id);
}

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int reduceBalance(Integer id, BigDecimal money) {
    String sql = "update t_balance set t_money = t_money - ? where t_id = ? ;";

    //模拟异常:

// int i = 1 / 0;

    return jdbcTemplate.update(sql, money, id);
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
实验一:两个被调用的事务方法正常执行,调用者出现异常:
修改Service层的transfer(),模拟异常:
//转账业务:用户1给用户2转账100元
@Transactional
public void transfer(){
//用户1 减少 100元
balanaceDao.reduceBalance(1, new BigDecimal(100));

    //模拟异常:
    int i = 1/0;

    //用户2 增加 100元
    balanaceDao.addBalance(2, new BigDecimal(100));

}

1
2
3
4
5
6
7
8
9
10
11
12
13
Dao层的两个方法,保持不变

执行,查看数据库结果:
IDEA报算术异常:

数据库中的数据如下:

可以发现,数据库中的数据发生了更新。

说明:因为Dao层的两个事务方法的事务传播行为都是REQUIRES_NEW,也就是说,虽然调用者transfer() 本身也是个事务方法,开启了自己的事务。但当调用到Dao层的两个方法时,由于传播行为,所以两个Dao层的方法都会开启自己的事务,不会和transfer()公用同一个事务。因此,当调用reduceBalance()时,transfer() 的事务被挂起(可以理解为线程阻塞,即暂停了),而reduceBalance()开启了个独立的新事务,执行自己的数据库操作,最后顺序执行完毕,提交了事务然后结束调用,transfer()的事务得以继续往下执行,往下执行后,遇到 算术异常,于是事务transfer()所在的事务回滚,程序终止。
特别注意,此时虽然transfer()的事务发生了回滚,但由于reduceBalance()是独立新开一个事务,并且顺利完成后提交了事务,因此transfer()的事务回滚并不影响reduceBalance() 的数据库操作结果。这就是所谓的分道扬镳。

实验二:两个被调用的事务方法正常执行,改变调用者出现异常的位置:
仍然是对数据库中的数据进行还原,然后开始操作:

改变Service层的代码如下:
//转账业务:用户1给用户2转账100元
@Transactional
public void transfer(){
//用户1 减少 100元
balanaceDao.reduceBalance(1, new BigDecimal(100));

    //用户2 增加 100元
    balanaceDao.addBalance(2, new BigDecimal(100));

    //模拟异常:
    int j = 1/0;
}

1
2
3
4
5
6
7
8
9
10
11
12
dao层的代码保持不变,两个事务方法的事务传播行为都是REQUIRES_NEW

查看运行结果
IDEA报算术异常:

查看数据库中的数据:

不出意外,两个Dao层的方法的修改都生效了。

说明:因为两个Dao层的方法都是事务方法,且传播行为为REQUIRES_NEW,因此当被调用时,会独立新开一个自己的事务进行数据库操作,将被调用者(即 transfer())的事务挂起,当Dao层的两个事务方法各自执行完数据库操作后,会立即提交自己的事务,数据库的修改立即生效。后面transfer() 虽然发生了算术异常,它所在的事务回滚,也不影响之前已经提交了的事务。

实验三:调用者正常执行,让第一个被调用者出现异常:
将数据库中的数据进行还原,然后实验:

修改Dao层中的reduceBalance(),开启模拟异常,addBalance() 保持不变:
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int reduceBalance(Integer id, BigDecimal money) {
String sql = “update t_balance set t_money = t_money - ? where t_id = ? ;”;

    //模拟异常:
    int i = 1 / 0;

    return jdbcTemplate.update(sql, money, id);
}

1
2
3
4
5
6
7
8
9
10
Service层的代码关闭模拟异常,使其正常执行:
//转账业务:用户1给用户2转账100元
@Transactional
public void transfer(){
//用户1 减少 100元
balanaceDao.reduceBalance(1, new BigDecimal(100));

    //用户2 增加 100元
    balanaceDao.addBalance(2, new BigDecimal(100));
    
}

1
2
3
4
5
6
7
8
9
10
查看运行结果:
IDEA依然报错:

查看数据库结果:

并未发生任何修改。

说明:reduceBalance() 和 addBalance() 虽然都会开启独立的事务,不与transfer() 共享同一个事务。但由于transfer() 是 先调用了 reduceBalance() ,而 reduceBalance() 中出现了异常,且未被进行处理,因此当它出现了异常后,reduceBalance() 自己的事务就会被回滚,接着异常被传递到了调用者,也就是transfer()中,而它也未进行处理,因此它所在的事务也会被回滚,而addBalance() 因为异常而未能被执行到,所以最后的结果就是数据库并未发生任何修改。

实验四:调用者正常执行,让第二个被调用者出现异常:
将数据库中的数据还原,然后开始实现:

Dao层中的reduceBalance() 关闭模拟异常,而addBalance()开启模拟异常,两者都是事务方法,事务传播行为都为REQUIRES_NEW:
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int addBalance(Integer id, BigDecimal money) {
String sql = “update t_balance set t_money = t_money + ? where t_id = ? ;”;

    //模拟异常:
    int i = 1 / 0;

    return jdbcTemplate.update(sql, money, id);
}

1
2
3
4
5
6
7
8
9
10
Service层的transfer()保持不变

运行查看结果:
IDEA仍然报错:

数据库中的数据发生了更改:

可以看到,reduceBalance() 操作生效了。

说明:借鉴实验三,我们其实已经可以理解了,reduceBalance() 独立开启事务操作数据库后提交了事务;接着 addBalance() 独立开启事务操作数据库,但操作过程中发生了算术异常,事务回滚,且由于未对异常进行操作,因此异常传递给了调用者即transfer(),而调用者也未对异常进行处理,因此调用者的事务也同样发生了回滚,程序结束。

总结:
当事务的属性 propagation 为 REQUIRES_NEW 时,该事务方法不仅必须运行在事务中,而且还必须是运行在自己新开的事务中(重新获取一条新的数据库连接)。若调用者(调用了该事务方法的方法)本身已开启了事务(即已经获取了数据库的连接),该事务方法也不会与它共用同一个事务(即同一条数据库连接)。若调用者自身不是事务方法,没有开启事务,那么该事务方法会自己获取一条数据库连接以开启事务,并在执行完自己的数据库操作后关闭该连接。

   除此之外,对于REQUIRES_NEW的事务,其是否会影响到调用者的事务,要根据异常是否被处理而定。

画个示意图:

核心总结:整个混合嵌套事务里,任何处出现异常,在异常出现前,已经执行的REQUIRES_NEW事务都会成功。

几个注意点:
如果是REQUIRED,子事务(被调用者)的属性都是继承于大事务(调用者)的;当子事务与大事务的属性不一致时,以大事务的为准。
而REQUIRES_NEW子事务可以调整自己的事务属性,不与大事务保持一致。
底层实现:
REQUIRED,是将调用者所在的大事务所使用的数据库连接传递给被调用的事务方法使用。
REQUIRES_NEW:该事务方法直接重新获取一个新的数据库连接进行使用。
本类方法的内部嵌套调用,事务控制不起作用,全程都是一个事务,即最外层的调用者的大事务。
原因:事务管理的底层是AOP,AOP的底层是动态代理,只有经过了动态代理对象调用方法,才能使方法被增强,也就是被加上事务管理。而类的内部进行方法嵌套调用,并没有通过代理对象进行方法的调用,因此不走代理模式,也就是AOP没有起到作用,所以事务管理也无法起作用。

   好了,以上就是我个人对本次内容的理解与解析,如果有什么不恰当的地方,还望各位兄弟在评论区指出哦。
   如果这篇文章对你有帮助的话,不妨点个关注吧~
   期待下次我们共同讨论,一起进步~

文章知识点与官方知识档案匹配,可进一步学习相关知识
Java技能树使用JDBC操作数据库数据库操作104383 人正在系统学习中

阿龟在奔跑
关注

3

13

1

Spring中事务传播行为的介绍
08-26
今天小编就为大家分享一篇关于Spring中事务传播行为的介绍,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
Spring中事务的传播行为有哪些
weixin_49349744的博客
685
————————————————
版权声明:本文为CSDN博主「阿龟在奔跑」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43719791/article/details/120813182

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值