工作原因是,摸鱼的时分在某渠道刷到一篇spring业务相关的博文,文章最后贴了一张图。里边关于嵌套业务的表述显着是过错的。
更奇怪的是,这张图有点形象。在必应搜索要害词PROPAGATION_NESTED
出来的榜首篇文章,里边就有这这部份内容,也是结尾部份完全如出一辙。
更要害的是,人家原文是表格,这位倒好,估计是怕麻烦,直接给截成图片了。
而且这篇文章其实在评论区现已被人指出来这方面的问题了,谁也不能保证自己写的文章没有一点纰漏,改了不就好了。但原作者并没有加以理会并修改过错。 一起,这位转载作者依然不加验证的直接拿走了。
这位转载作者可不是个小号,是某年度的人气作者。
可能是有自己的大众号,得保持必定的更新频率?
好家伙,没经过验证,一部份过错的内容就这样被持续扩展传达了。
在必应搜索要害词PROPAGATION_NESTED
出来文章,前两篇都是CSDN,都是相同的文章相同的过错。另外几篇文章也或多或少有些表述不清的当地。因而尝试来写一写这方面的东西。
趁便吐槽一下CSDN,我好多篇文章都被这上面的某些作者给扒曩昔,然后搜索如出一辙的标题,权重比我还高,出来排榜首位的反而是CSDN的盗版文章。
1.当咱们在议论嵌套业务的时分,嵌套的是什么?
当看到`嵌套业务`榜首反响想到是这款式的:
但这更像PROPAGATION_REQUIRES_NEW
啊,感兴趣能够去打断点履行一下。PROPAGATION_REQUIRES_NEW
业务传达下,办法A调用办法B便是这样,
// 业务A doBegin()
// 业务B doBegin()
// 业务B doCommit()
// 业务A doCommit()
而在PROPAGATION_NESTED
业务传达下,打了个断点,会发现只会履行一次doBegin和doCommit:
业务A doBegin()
业务A doCommit()
咱们用代码输出愈加直观。
界说两个办法serviceA和serviceB,运用前者调用后者。前者业务传达运用REQUIRED
,后者运用PROPAGATION_NESTED
。
@Transactional(propagation = Propagation.REQUIRED)
public void serviceA(){
Tcity tcity2 = new Tcity();
tcity2.setId(0);
tcity2.setStateCode("5");
tcity2.setCnCity("测验城市2");
tcity2.setCountryCode("ALB");
tcityMapper.insertSelective(tcity2);
transactionInfo();
test2.serviceB();
}
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)
public void serviceB() {
Tcity tcity = new Tcity();
tcity.setId(0);
tcity.setStateCode("5");
tcity.setCnCity("测验城市");
tcity.setCountryCode("ALB");
tcityMapper.insertSelective(tcity);
tcityMapper.selectAll2();
transactionInfo();
这儿的transactionInfo()运用业务同步器管理器TransactionSynchronizationManager
注册一个业务同步器TransactionSynchronization
。
这样在业务完成之后afterCompletion
会输出当时业务是commit
还是rollback
,这样也便于测验,比起去刷新数据库看有没有写入,愈加方便快捷直观。
一起运用TransactionSynchronizationManager.getCurrentTransactionName()
能够得到当时业务的称号,这样能够直观的看到当时办法运用的是同一个业务还是不同的业务。
protected void transactionInfo() {
String transactionName = TransactionSynchronizationManager.getCurrentTransactionName();
boolean active = TransactionSynchronizationManager.isActualTransactionActive();
log.info("transactionName:{}, active:{}", transactionName, active);
if (!active) {
log.info("transaction :{} not active", transactionName);
return;
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
if (status == STATUS_COMMITTED) {
log.info("transaction :{} commit", transactionName);
} else if (status == STATUS_ROLLED_BACK) {
log.info("transaction :{} rollback", transactionName);
} else {
log.info("transaction :{} unknown", transactionName);
}
}
});
}
履行测验代码:
@RunWith(SpringRunner.class)
@SpringBootTest
public class Test {
@Autowired
private Test1 test1;
@org.junit.Test
public void test(){
test1.serviceA();
}
}
输出:
能够非常直观地观察到3点状况:
1.经过上图标记为1的当地,能够看到两个办法运用了一个业务com.nyp.test.service.propagation.Test1.serviceA
。
2.经过上图标记为2的当地,以及箭头次序,能够看到业务履行次序类似于(事实上不是,只是业务同步器的问题,下文有阐明):
// 业务A doBegin()
// 业务B doBegin()
// 业务A doCommit()
// 业务B doCommit()
3.经过业务同步器打印日志发现commit履行了两次。
以上2,3两点与前面打断点的定论貌似是有点冲突。
1.1嵌套业务终究有几个业务
源码版别:spring-tx 5.3.25
经过源码,能够很直观地观察到,useSavepointForNestedTransaction()
默许回来true,这样就不会敞开一个新的业务(startTransaction
), 而是创立一个新的savepoint
。
相当于在办法A的时分会敞开一个新的业务,在调用办法B的时分,会在办法A之后办法B之前创立一个检查点。
类似于在本来的A办法上手动添加检查点。
@Transactional(propagation = Propagation.REQUIRED)
public void serviceA(){
Object savePoint = null;
try {
Tcity tcity2 = new Tcity();
tcity2.setId(0);
tcity2.setStateCode("5");
tcity2.setCnCity("测验城市2");
tcity2.setCountryCode("ALB");
tcityMapper.insertSelective(tcity2);
transactionInfo();
savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
test2.serviceB();
} catch (Exception exception) {
exception.printStackTrace();
TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
}
}
然后经过检查点,将一个逻辑业务
分为多个物理业务
。
我这可不是在乱讲啊,我是有备而来。
github.com/spring-proj…
上面是spring 在github官方社区07年的一个贴子,Juergen Hoeller
有一段回复。
Juergen Hoeller
是谁?他是spring的联合创始人,业务这一块的首要开发者。
PROPAGATION_NESTED的不同之处在于,它运用具有多个保存点的单个物理业务,能够回滚到这些保存点。这种部分回滚允许内部业务规模触发其规模的回滚,而外部业务能够持续进行物理业务,虽然现已回滚了一些操作。这通常映射到JDBC保存点上,因而只适用于JDBC资源业务(Spring的DataSourceTransactionManager)。
在嵌套业务中,全体是一个逻辑业务,经过savepoint在jdbc物理层面把调用办法分割成一个个的物理业务。
由于spring层面只有一个逻辑业务,所以经过断点只履行了一次doBegin()和doCommit(),但实际上履行了两次preCommit(),假如有savepoint那就不履行commit(),
这也能答复上面2,3两点问题的疑问。
所以上面办法A调用办法B进行嵌套业务,右图比左图更形象精确:
1.2 savepoint
savepoint是JDBC的一种机制,spring运用savepoint来完成了嵌套业务。
在数据库操作中,默许autocommit为true,意味着一条SQL一个业务。也能够将autocommit设置为false,将多条SQL组成一个业务,一起commit或者rollback。
以上都是惯例操作,在一个业务中所以数据库操作全部捆绑在一起。在某些特定状况下,在一个业务中,用户只希望rollback其中某部份,这时分能够用到savepoint。
记咱们遗忘@Transactional
,以编程式业务的方式来手动设置一个savepoint。
办法A,写入一条用户记载,并设置一个检查点。
@Autowired
private PlatformTransactionManager platformTransactionManager;
public void serviceA(){
TransactionStatus status = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
Object savePoint = null;
try {
Person person = new Person();
person.setName("张三");
personDao.insertSelective(person);
transactionInfo();
// 设置一个savepoint
savePoint = status.createSavepoint();
test2.serviceB();
} catch (Exception exception) {
exception.printStackTrace();
// 这儿输出两次commit,到rollback到51行,会插入一条数据
status.rollbackToSavepoint(savePoint);
// 这儿会两次rollback
// platformTransactionManager.rollback(status);
}
platformTransactionManager.commit(status);
}
办法B写入一条日志记载。并在此模仿一个反常。
public void serviceB() {
TLog tLog = new TLog();
tLog.setOprate("user");
transactionInfo();
tLogDao.insertSelective(tLog);
int a = 1 / 0;
}
测验希望到达的作用是,日志写入失利,但用户记载写入成功。很显着,假如不运用savepoint是达不到的。由于两个办法是一个业务,在办法B中报错了,抛出反常,用户和日志的数据库操作都将回滚。
测验输出日志:
[2023-04-24 14:40:18.740] INFO 88384 [main] [com.nyp.test.service.propagation.Test1] : transactionName:null, active:true
[2023-04-24 14:40:18.742] INFO 88384 [main] [com.nyp.test.service.propagation.Test2] : transactionName:null, active:true
java.lang.ArithmeticException: / by zero
......省略
[2023-04-24 14:40:18.747] INFO 88384 [main] [com.nyp.test.service.propagation.Test1] : transaction :null commit
[2023-04-24 14:40:18.747] INFO 88384 [main] [com.nyp.test.service.propagation.Test2] : transaction :null commit
数据库也表明用户写入成功,日志写入失利。
2.一开始的问题,B先回滚A再正常提交?
本文开始的问题是办法A业务传达为PROPAGATION_REQUIRED
,办法B业务传达为PROPAGATION_NESTED
。办法A调用B,methodA正常,methodB抛反常。
这种状况下会发生什么?
B先回滚,A再正常提交
这种说法为什么会有问题,有什么问题?
2.1 先B后A的次序有问题吗?
经过前面业务同步器打印的日志咱们得知,业务以test1.serviceA()履行doBegin(),test2.serviceB()履行doBegin(),test1.serviceA()履行doCommit(),test2.serviceB()履行doCommit()
这样的次序履行。
可是果真如此吗?
经过源码咱们首先得知,preCommit()在commit()办法之前,在preCommit()会做savepoint的判别,假如有检查点就不履行commit()。
- 一起办法B只是一个savepoint不是一个真实的业务,并不会履行业务同步器。
- 办法A是一个真实的业务,所以会履行commit(),一起也会履行上面的业务同步器。
这儿的业务同步器是一个Arraylist,它的履行次序即是arraylist的遍历次序,只是只代表参加的先后,并不代表业务真实commit/rollback的次序。
从1,2两点能够得出定论,先B后A的次序并没有问题。
一起,依据1,在嵌套业务中运用业务同步器要特别当心,在检查点的时分并不会履行同步器,一起会掩盖真实的操作。
比如办法B回滚了,但由于办法B只是个savepoint,所以业务同步器不会履行。等到办法A履行完操作业务同步器的时分,也只会反响外层业务即办法A的业务成果。
2.2 真实的问题
假如B回滚,A是commit还是rollback取决于办法A是否持续把反常往上抛。
让咱们先暂时遗忘嵌套业务,测验一个REQUIRES_NEW的事例。
同样的办法A业务传达为REQUIRES
,办法B为REQUIRES_NEW
。
此刻办法A和办法B为两个互相独立的业务。
办法A调用办法B,办法B抛出反常。
此刻,办法B肯定会回滚,但办法A呢?按理说互相独立,那肯定是commit了。
但真的如此吗?
(1). 办法A不做反常处理。
测验成果:
能够看到确实是两个业务,但两个业务都rollback了。由于办法A虽然没有报反常,但它接到了办法B的反常且往上抛了,spring只会以为办法A同样也抛出了反常。因而两个业务都需要回滚。
(2).办法A处理了反常。
将办法A代码try-catch住,再履行。
日志有点多不做截图,
[2023-04-24 16:10:30.669] INFO 96664 [main] [com.nyp.test.service.propagation.Test1] : transactionName:com.nyp.test.service.propagation.Test1.serviceA, active:true
[2023-04-24 16:10:30.672] INFO 96664 [main] [com.nyp.test.service.propagation.Test2] : transactionName:com.nyp.test.service.propagation.Test2.serviceB, active:true
[2023-04-24 16:10:30.687] INFO 96664 [main] [com.nyp.test.service.propagation.Test2] : transaction :com.nyp.test.service.propagation.Test2.serviceB rollback
java.lang.ArithmeticException: / by zero
省略
[2023-04-24 16:10:30.689] INFO 96664 [main] [com.nyp.test.service.propagation.Test1] : transaction :com.nyp.test.service.propagation.Test1.serviceA commit
能够看到两个单独的业务,业务B回滚了,业务A提交了。
虽然咱们这末节说的是REQUIRES_NEW
,但嵌套业务是相同的道理。
假如B回滚,当办法A持续往上抛反常,则A回滚;当办法A处理了反常不往上抛,则A提交。
3. 场景
在2.2末节中,咱们举了REQUIRES_NEW
的例子来阐明,有的同学可能就会有点疑问了。既然业务B回滚了,业务A都要依据状况来判别是否回滚,那这样嵌套业务跟REQUIRES_NEW
有啥区别?
还是拿注册的场景来说。往数据库写1条用户记载,再写1条注册成功操作日志。
-
假如日志写入失利,用户写入不受影响。这种状况下,
REQUIRES_NEW
和嵌套业务都能完成。而且很显着REQUIRES_NEW
还没那么弯弯绕绕。 -
考虑另外一种状况,假如用户写入失利了,那这时分我想要日志写入也失利。由于用户都没了,就不存在注册操作成功的操作日志了。
场景1
办法A传达级别为REQUIRED,并模仿一个反常。
@Transactional(propagation = Propagation.REQUIRED)
public void serviceA(){
Person person = new Person();
person.setName("李四");
personDao.insertSelective(person);
transactionInfo();
test2.serviceB();
int a = 1 / 0;
}
在办法B为REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRED_NEW)
public void serviceB() {
TLog tLog = new TLog();
tLog.setOprate("user");
transactionInfo();
tLogDao.insertSelective(tLog);
}
打印输出
能够看到办法B提交了,也便是说用户注册失利了,但用户注册成功的操作日志却写入成功了。
场景2
咱们再来看看嵌套业务的状况下: 办法A传达级别为REQUIRED,并模仿一个反常。
@Transactional(propagation = Propagation.REQUIRED)
public void serviceA(){
Person person = new Person();
person.setName("李四");
personDao.insertSelective(person);
transactionInfo();
test2.serviceB();
int a = 1 / 0;
}
办法B业务传达级别为NESTED。
@Transactional(propagation = Propagation.NESTED)
public void serviceB() {
TLog tLog = new TLog();
tLog.setOprate("user");
transactionInfo();
tLogDao.insertSelective(tLog);
}
履行日志
能够看到同一个逻辑业务下的两段物理业务都回滚了,到达了咱们预期的作用。
4.小结
1.办法A业务传达为REQUIRED,办法B业务传达为NESTED。办法A调用办法B,当B抛出反常时,
假如A处理了反常,此刻业务A提交。否则,业务A回滚。
2.REQUIRED_NEW和NESTED在有些场景下能够完成相同的功能,但在某些特定场景下只能NESTED完成。
3.NESTED底层逻辑是JDBC的savepoint。父业务类似于一个逻辑业务,savepoint将各办法分割了若干物理业务。
4.在嵌套业务中运用业务同步器时需要特别当心。
看到这儿点个赞呗`