引言
在介绍正文之前,让我们先一起来看下这段代码:
@Transactional
public void createProduct(Long skuId, Integer number, Long operatorUcid) {
// 插入商品信息
recordProduct(skuId, number);
// 插入商品操作记录日志
recordProductOperateLogClass.recordProductOperateLog(skuId, operatorUcid);
}
public void recordProduct(Long skuId, Integer number) {
// 插入 商品的操作记录日志
ProductPO productPO = new ProductPO();
productPO.setSkuId(Math.toIntExact(skuId));
productPO.setNumber(number);
productMapper.insert(productPO);
}
@Service
public class RecordProductOperateLogClass{
@Resource
ProductOperateLogMapper productOperateLogMapper;
@Transactional
public void recordProductOperateLog(Long skuId, Long operatorUcid) {
// 插入 商品的操作记录日志
ProductOperateLog productOperateLog = new ProductOperateLog();
productOperateLog.setSkuId(Math.toIntExact(skuId));
productOperateLog.setOperatorUcid(String.valueOf(operatorUcid));
productOperateLogMapper.insert(productOperateLog);
//故意抛错代码
double b = 1 / 0;
}
}
可以看到,这个是一个创建商品的流程时。在createProduct方法中,我们开启了第一个事务A。而在记录商品操作记录recordProductOperateLog时,我们又开启了一个新的事务B。 由此我们发现,我们创建了两个事务,而且外层的事务A包裹着内层的事务B,这就被称为事务嵌套。
那么问题来了,这两个事务相互间的关系是怎样的呢?是共用一个事务?还是两个完全不同的独立的事务呢?这就是我们本期文章要介绍的内容:事务传播机制。
事务传播简介
事务传播分类
结合上述的铺垫描述,事务传播机制可以定义如下:在事务嵌套的情况下,事务如何从调用者往被调用者传播的机制。
首先,需要清楚的是,根据场景的不同,所需要使用的事务传播机制往往是不同的。比如对于数据强一致性的情况,我们希望这两个嵌套事务保持一致。但是对一些数据不需要保持强一致性的场景,我们可能希望两个事务间互不干扰。因此,根据使用场景的不同,事务传播被分成了七个不同的使用等级,如下所示:
事务传播级别 | 代码中的传播级别 | 级别描述 |
---|---|---|
REQUIRED | PROPAGATION_REQUIRED | REQUIRED级别是Spring的默认事务级别,在Spring注释中对于该等级的描述是:会自动加入当前的事务,如果不存在则新建事务。 |
SUPPORTS | PROPAGATION_SUPPORTS | SUPPORT级别,跟REQUIRED类似,也是会自动加入当前的事务,但两者区别在于,若当前无事务,则按照无事务的方式执行。 |
MANDATORY | PROPAGATION_MANDATORY | MANDATORY级别,要求SQL操作强制在事务内部执行,如果当前不存在事务,则会抛出错误 |
REQUIRES_NEW | PROPAGATION_REQUIRES_NEW | 该级别同REQUIRES_NEW相似,唯一不同的是,它不会加入已有的事务,而是每次都把事务挂起,新建一个事务,等到事务执行完成了在恢复原有事务的执行。 |
NOT_SUPPORTED | PROPAGATION_NOT_SUPPORTED | 该级别默认不支持事务,如果当前调用者已经有事务了,那么此时会将事务挂起,待SQL执行完后,重新返回事务节点。 |
NEVER | PROPAGATION_NEVER | 该级别标志着SQL语句绝不在事务中操作,若当前在事务中,则直接抛出异常,刚好同MANDATORY级别相对。 |
NESTED | PROPAGATION_NESTED | 该级别同REQUIRES_NEW比较相似,都是会新起一个事务。然而不同点在于REQUIRES_NEW每次都创建新的独立的物理事务,而Nested只有一个物理事务;Nested嵌套事务回滚或提交不会导致外部事务回滚或提交,但外部事务回滚将导致嵌套事务回滚,而RequiresNew由于都是全新的事务,所以之间是无关联的。 |
上述介绍的事务传播机制还是很容易看懂各自的作用的,唯一需要注意的是NESTED机制,其跟REQUIRES_NEW机制很像。在没有事务的时候,两者都会新创建一个事务。但是如果在存在事务的时候,两者就出现了差异:
很容易看到,REQUIRES_NEW始终会新建事务,但是NESTED则可能通过在一个事务内新增加保存点的方式。因此也可能导致外层的保存点1出现问题的时候,会让所有的事务内容都进行回滚。
总结下来,在存在不同的场景下,对应使用的事务传播类型各有偏差。具体使用的事务传播级别,需要根据各自使用场景方可决定。
事务传播实现原理
要实现事务传播,很自然应该分为两步:
1、设置事务传播等级;
2、根据事务传播等级判断选择的事务。
简单介绍一下上述两步的含义,由于事务传播机制是由用户自主选择的、根据场景区分的,因此自然需要提供给用户选择的余地。在用户选择的事务传播等级后,只需要根据事务传播等级选择不同的操作即可。下面我们就围绕这两步来介绍事务传播的实现原理。
设置事务传播等级
声明式事务
对于声明式事务来说,设置事务传播等级的方式大家应该都不陌生。通过设置Transactional中的propagation变量就可以使得代码生效。
@Transactional(propagation = Propagation.NOT_SUPPORTED)
然而问题来了,在注解中设置的这个变量,是怎么被Spring识别到的呢?这就不得不提到声明式事务一个非常重要的类TransactionInterceptor了。
(不了解这个类的同学可以参考下我之前写的文章《浅析Spring事务实现原理》)
TransactionInterceptor有一个非常重要的点,就是继承了类TransactionAspectSupport。事务执行时会调用到TransactionInterceptor中的invoke方法,会紧接着调用父类TransactionAspectSupport中的invokeWithinTransaction方法,在该方法里有一个十分关键的点,就是会去查询当前的事务属性信息:
紧跟着查询查找属性的代码内容,这里要追的链路很长,限于篇幅原因,我就把一些关键的节点标注成链路了,有兴趣的同学可以逐一展开查看。
最终的话,咱们可以找到关键的类SpringTransactionAnnotationParser,其下有一个方法parseTransactionAnnotation,其具体代码如下所示:
protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) {
RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute();
Propagation propagation = attributes.getEnum("propagation");
rbta.setPropagationBehavior(propagation.value());
Isolation isolation = attributes.getEnum("isolation");
rbta.setIsolationLevel(isolation.value());
rbta.setTimeout(attributes.getNumber("timeout").intValue());
String timeoutString = attributes.getString("timeoutString");
Assert.isTrue(!StringUtils.hasText(timeoutString) || rbta.getTimeout() < 0,
"Specify 'timeout' or 'timeoutString', not both");
rbta.setTimeoutString(timeoutString);
rbta.setReadOnly(attributes.getBoolean("readOnly"));
rbta.setQualifier(attributes.getString("value"));
rbta.setLabels(Arrays.asList(attributes.getStringArray("label")));
List<RollbackRuleAttribute> rollbackRules = new ArrayList<>();
for (Class<?> rbRule : attributes.getClassArray("rollbackFor")) {
rollbackRules.add(new RollbackRuleAttribute(rbRule));
}
for (String rbRule : attributes.getStringArray("rollbackForClassName")) {
rollbackRules.add(new RollbackRuleAttribute(rbRule));
}
for (Class<?> rbRule : attributes.getClassArray("noRollbackFor")) {
rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
}
for (String rbRule : attributes.getStringArray("noRollbackForClassName")) {
rollbackRules.add(new NoRollbackRuleAttribute(rbRule));
}
rbta.setRollbackRules(rollbackRules);
return rbta;
}
很清晰可以看出来,就是在这里将我们所设置的每一个变量给获取出来的。咱们设置的propagation、value、rollbackFor,都是通过SpringTransactionAnnotationParser解析出来并保存下来的。
编程式事务
在事务传播机制的设计上,编程事务则相对简单。通过setPropagationBehavior()方法,就可以设置事务传播的级别。
transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRED);
在后续也是直接通过getPropagationBehavior方法直接获取到对应的事务传播级别。
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}
根据事务传播等级判断
在实现了事务等级的设置之后,我们只需要根据设置好的事务等级选择事务传播方案即可,如:是否加入当前的事务?是否要挂起当前的事务?
对于编程式和声明式事务来说,两者关于事务传播机制的实现,其实最终都统一由AbstractPlatformTransactionManager下的方法getTransaction上。其具体源代码如下所示:
@Override
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException {
......
Object transaction = doGetTransaction();
//判断当前是否有事务
if (isExistingTransaction(transaction)) {
// 如果存在事务,则走不一样的判断逻辑
return handleExistingTransaction(def, transaction, debugEnabled);
}
......
// 如果当前没有事务 and 传播级别是PROPAGATION_MANDATORY 则会直接报错
if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
//判断当前如果是 REQUIRD/REQUIRD_NEW/NESTED 则走该逻辑新建事务
SuspendedResourcesHolder suspendedResources = suspend(null);
try {
//开启新事务执行
return startTransaction(def, transaction, debugEnabled, suspendedResources);
}catch (RuntimeException | Error ex) {
resume(null, suspendedResources);
throw ex;
}
}else {
// 剩余的 NEVER/NOT_SUPPORTED/SUPPORTS则走下面的逻辑,此时只开启empty事务(非真的事务),只是为了保持同步。
......
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
}
}
从getTransaction的源码中不难看到,在存在事务的时候其实是直接执行了handleExistingTransaction的方法。这里我们再引入方法handleExistingTransaction的源码:
private TransactionStatus handleExistingTransaction(TransactionDefinition definition,
Object transaction, boolean debugEnabled) throws TransactionException {
// 如果是NEVER,此时存在事务,直接报错!
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
throw new IllegalTransactionStateException(
"Existing transaction found for transaction marked with propagation 'never'");
}
// 如果是NOT_SUPPORTED,那么此时会挂起原事务,并保存挂起的holder。同时开启empty事务,保持同步
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
......
Object suspendedResources = suspend(transaction);
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(
definition, null, false, newSynchronization, debugEnabled, suspendedResources);
}
// 如果是REQUIRES_NEW,那么此时悬挂旧事务,同时开启新事务
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
......
SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {
//依旧开启新事务
return startTransaction(definition, transaction, debugEnabled, suspendedResources);
}catch (RuntimeException | Error beginEx) {
resumeAfterBeginException(transaction, suspendedResources, beginEx);
throw beginEx;
}
}
// 如果此时是 NESTED,
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
// 如果不允许开启Nested事务,则报错。
if (!isNestedTransactionAllowed()) {
throw new NestedTransactionNotSupportedException(
"Transaction manager does not allow nested transactions by default - " +
"specify 'nestedTransactionAllowed' property with value 'true'");
}
......
if (useSavepointForNestedTransaction()) {
//使用保存点
DefaultTransactionStatus status =
prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
status.createAndHoldSavepoint();
return status;
}else {
// 否则开启事务
return startTransaction(definition, transaction, debugEnabled, null);
}
}
// 剩余的只有 SUPPORTS/REQUIRED/MANDATORY 传播级别 - 此时直接加入已有的事务执行即可
......
return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
}
上述的代码片段比较长,我将上述代码的相应的执行流程整理成了如下流程图:
从流程图中其实可以比较清晰的看到整个事务传播机制是如何判断出来的,以及是如何作用的。可以看到,主干主要分成了:当前存在事务和不存在事务两个分支。
不存在事务:
对于不存在事务的情况,首先判断是否为MANDATORY级别,如果是,则直接报错。这就确保了MANDATORY一定是需要在有事务的时候执行的。
紧接着判断是否为REQUIRED/REQUIRES_NEW/NESTED的级别,如果是这几个级别,则需要开启新事务执行。
如果是NEVER/NOT_SUPPORTED/SUPPORTS三个级别,那么此时选择按照非事务的方式执行。
存在事务:
存在事务的情况相较于不存在事务的情况相对更复杂一些,首先需要判断是否为NEVER级别,若是则直接报错。确保NEVER情况下不用事务进行。
紧接着判断是否为NOT_SUPPORT级别,如果是那么此时要悬挂当前的事务,并按照非事务的方式执行。
若上述两种都不满足,则再判断是否为REQUIRES_NEW级别,如果是,那么也是需要悬挂原有的事务,并新建事务执行的。
然后如果上述依旧不满足,则再判断是否为NESTED级别,如果不是则直接按照当前事务执行(SUPPORT/REQUIRED/MANDATORY级别)的要求。
如果是NESTED级别的话,还需要简单判断下是否支持保存点,若支持,则采用保存点保存,否则新建事务保存。
总结
在本篇文章中,将事务传播机制的设计背景、等级分类及实现原理一一拆解开来。由此我们就可以在日后的实现过程中,能够更符合场景地选择我们所需的事务传播级别。
参考文献
事务的传播机制
Spring事务传播机制