原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。
Spring有5种隔离级别,7种传播行为。这是面试常问的内容,也是代码中经常碰到的知识点。这些知识枯燥而且乏味,其中有些非常的绕。如果栽在这上面,就实在是太可惜了。
xjjdog在一些事务的基础上,再探讨几个容易淡忘的概念,从源码层面找原因,加深我们的理解,问题大概包括:
Spring的事务和数据库的事务隔离是一个概念么?
Spring是如何实现事务的?
事务隔离机制都有哪些?
事务传播机制都有哪些?
查询语句需要开事务么?
private方法加事务注解有用么?
先来第一个问题,Spring的事务隔离级别和数据的事务隔离级别,是一回事么?
其实,数据库一般只有4种隔离机制,Spring抽象出一种default,根据数据设置来变动。
read uncommitted(未提交读)
read committed(提交读、不可重复读)
repeatable read(可重复读)
serializable(可串行化)
default (PlatformTransactionManager默认的隔离级别,使用的就是数据库默认的)
这是因为,Spring只提供统一事务管理接口,具体实现都是由各数据库自己实现(如MySQL)。Spring会在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。
在DataSourceUtils
文件中,代码详细的输出了这个过程。
// Apply specific isolation level, if any.
Integer previousIsolationLevel = null;
if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
if (logger.isDebugEnabled()) {
logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " +
definition.getIsolationLevel());
}
int currentIsolation = con.getTransactionIsolation();
if (currentIsolation != definition.getIsolationLevel()) {
previousIsolationLevel = currentIsolation;
con.setTransactionIsolation(definition.getIsolationLevel());
}
}
结论:三种情况,如果Spring没有指定事务隔离级别,则会采用数据库默认的事务隔离级别;当Spring指定了事务隔离级别,则会在代码里将事务隔离级别修改为指定值;当数据库不支持这种隔离级别,效果则以数据库的为准(比如采用了MyISAM引擎)。
我们会使用如下的方式进行声明。如果不是有性能和需求问题,就不要瞎改。事务处理弄不好是会锁表的,而锁表在大并发的情况下是会死人的。
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
只要写代码,代码总会存在嵌套,或者循环,造成了事务的嵌套或者循环。那么事务在这些情况下,根据配置会有不同的反应。
REQUIRED 这是默认的。表示当前方法必须在一个具有事务的上下文中运行,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚)
REQUIRE_NEW 表示当前方法必须运行在它自己的事务中。如果存在当前事务,在该方法执行期间,当前事务会被挂起
NESTED 如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同required
的一样
SUPPORTS 表示当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行
NOT_SUPPORTED 表示该方法不应该在一个事务中运行。如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行
MANDATORY 表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常
NEVER 表示当方法务不应该在一个事务中运行,如果存在一个事务,则抛出异常
一般用得比较多的是REQUIRED
, REQUIRES_NEW
,用到其他的,你就要小心了,搞懂再用。
最怕如果
这俩字了,它会将事情搞的很复杂,尤其是代码量大的时候,你永远不知道你写的service
会被谁用到。这就很尴尬了。
我们会采用下面的方式进行声明。鉴于Spring的事务传播非常的绕,如果功能满足需求,那么就用默认的就好,否则会引起不必要的麻烦。
@Transactional(propagation=Propagation.REQUIRED)
事务传播机制看似神奇,实际上是使用简单的ThreadLocal
的机制实现的。所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。这不同于我们以前讲到的透传,Spring并没有做这样的处理。
参考:你的也是我的。3例ko多线程,局部变量透传
所以事务传播机制,只有翻一遍源代码,才能印象深刻。仅靠文字去传播,很多东西会变得不可描述。
如图,PlatformTransactionManager只有简单的三个抽象接口,定义了包含JDBC在内的Spring所有的事务操作。
我们平常说的JDBC,只是占到其中一部分。
实现的方式,依然是使用AOP
来实现的,具体的实现类是由TransactionAspectSupport
来实现的。可以看到,代码定义了一个叫做transactionInfoHolder
的ThreadLocal变量,当用到它的时候,就能够确保在同一个线程下,获取的变量是一致的。
/**
* Holder to support the {@code currentTransactionStatus()} method,
* and to support communication between different cooperating advices
* (e.g. before and after advice) if the aspect involves more than a
* single method (as will be the case for around advice).
*/
private static final ThreadLocal transactionInfoHolder =
new NamedThreadLocal<>("Current aspect-driven transaction");
具体的业务逻辑,是在invokeWithinTransaction
中实现的。如果你继续向下跟踪的话,会找到AbstractPlatformTransactionManager
类中的getTransaction
方法。
@Override
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException {
// Use defaults if no transaction definition given.
TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());
Object transaction = doGetTransaction();
boolean debugEnabled = logger.isDebugEnabled();
if (isExistingTransaction(transaction)) {
// Existing transaction found -> check propagation behavior to find out how to behave.
return handleExistingTransaction(def, transaction, debugEnabled);
}
// Check definition settings for new transaction.
if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
}
// No existing transaction found -> check propagation behavior to find out how to proceed.
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) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
}
try {
return startTransaction(def, transaction, debugEnabled, suspendedResources);
}
catch (RuntimeException | Error ex) {
resume(null, suspendedResources);
throw ex;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
logger.warn("Custom isolation level specified but no actual transaction initiated; " +
"isolation level will effectively be ignored: " + def);
}
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
}
}
不用我做过多解释了吧,一切明显的逻辑,都在代码里。事务就是在这里创建的。
事务有个readonly
,控制了事务的只读属性,和事务是否开启没半毛钱关系。
在以前的一篇文章中,谈到通过设置readonly属性来控制语句的路由:”MySQL官方驱动“主从分离的神秘面纱(扫盲篇),这其中就用到了事务的其中一个属性readonly
,它最终是体现在数据库连接层面的。
connection.setReadOnly(true);
在Spring中的使用方式如下:
@Transactional(readOnly=true)
值得注意的是,这个属性设置之后,并不是每个底层的数据库都支持。中间层的ORM或者驱动,也可能会拿这个属性做一些文章,所以与其说这个readonly
是功能性的,不如说是一种暗示
。
拿MySQL来说,有两种提交模式:
SET AUTOCOMMIT=0 禁止自动提交
SET AUTOCOMMIT=1 开启自动提交
这都是实打实的SQL语句,所以如果开启了事务,AUTOCOMMIT
要为false
。我们可以看到Spring做了以下几个操作。
con.setAutoCommit(false);
如果是只读事务,还不忘手动设置一下。
if (isEnforceReadOnly() && definition.isReadOnly()) { try (Statement stmt = con.createStatement()) {
stmt.executeUpdate("SET TRANSACTION READ ONLY");
}
}
这种操作是很昂贵的,如果不加Transaction注解,默认是不开启事务的。单条的查询语句也是没有必要开启事务的,数据库默认的配置就能满足需求。
但如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,就会造成数据的前后不一。
也仅有在这种情况下,要开启读事务。
@Transaction
注解加在private
上,并没有什么卵用。
这倒不是事务处理的代码去写的特性。由于事务的这些功能,是通过AOP
方式强加进去的,所以它收到动态代理的控制。
private
和final
修饰的方法,不会被代理。
但是,你却可以把private方法放在带有事务功能的public方法里。这样,它看起来
也有了事务的一些功能特性,但它并没有。
互联网中,用到的事务并不多,很多都是非常小、速度非常快的接口,对于开发人员来说,事务是个累赘。
但在一些带有金融属性的业务中,或者一些企业级开发应用中,事务确实一个绕不过的坎。一旦深入其中,就会发现这个知识点,露着血盆大口,等君入瓮。
xjjdog
从源码层次,聊到了几个面试常问的问题。不要觉得奇怪,有的人确实一直在拿着脏读、幻读这样的名词来面试。
而这些东西,都属于当时看了恍然大悟,第二天就继续懵逼的内容。
什么时候,才能务实一点呢?
近期热门文章
《996的乐趣,你是无法想象的》
魔幻现实主义,关爱神经衰弱
《一切荒诞的傲慢,皆来源于认知》
不要被标题给骗了,画面感十足的消遣文章
《必看!java后端,亮剑诛仙》
后端技术索引,中肯火爆。全网转载上百次。
《学完这100多技术,能当架构师么?(非广告)》
精准点评100多框架,帮你选型
作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流