39

再深一点:面试工作两不误,源码级理解Spring事务

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzA4MTc4NTUxNQ%3D%3D&%3Bmid=2650521159&%3Bidx=1&%3Bsn=81a44bc7e0217a320179ce4935b9b565
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

e6JBVrU.gif

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

Spring有 5 种隔离级别, 7 种传播行为。这是面试常问的内容,也是代码中经常碰到的知识点。这些知识 枯燥 而且 乏味 ,其中有些非常的绕。如果栽在这上面,就实在是太可惜了。

ni6vEfv.png!web

xjjdog在一些事务的基础上,再探讨几个容易淡忘的概念,从源码层面找原因,加深我们的理解,问题大概包括:

  1. Spring的事务和数据库的事务隔离是一个概念么?

  2. Spring是如何实现事务的?

  3. 事务隔离机制都有哪些?

  4. 事务传播机制都有哪些?

  5. 查询语句需要开事务么?

  6. private方法加事务注解有用么?

1、Spring的事务和数据库的事务隔离是一个概念么?

先来第一个问题,Spring的事务隔离级别和数据的事务隔离级别,是一回事么?

其实,数据库一般只有4种隔离机制,Spring抽象出一种default,根据数据设置来变动。

fY7VZfI.png!web

  • 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)

2、Spring事务的7种传播机制

只要写代码,代码总会存在嵌套,或者循环,造成了事务的嵌套或者循环。那么事务在这些情况下,根据配置会有不同的反应。

vmqm2mR.png!web

  • REQUIRED这是默认的。表示当前方法必须在一个具有事务的上下文中运行,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚)

  • REQUIRE_NEW表示当前方法必须运行在它自己的事务中。如果存在当前事务,在该方法执行期间,当前事务会被挂起

  • NESTED如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同 required 的一样

  • SUPPORTS表示当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行

  • NOT_SUPPORTED表示该方法不应该在一个事务中运行。如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行

  • MANDATORY表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常

  • NEVER表示当方法务不应该在一个事务中运行,如果存在一个事务,则抛出异常

一般用得比较多的是 REQUIREDREQUIRES_NEW ,用到其他的,你就要小心了,搞懂再用。

最怕 如果 这俩字了,它会将事情搞的很复杂,尤其是代码量大的时候,你永远不知道你写的 service 会被谁用到。这就很尴尬了。

我们会采用下面的方式进行声明。鉴于Spring的事务传播非常的绕,如果功能满足需求,那么就用默认的就好,否则会引起不必要的麻烦。

@Transactional(propagation=Propagation.REQUIRED)

3、事务传播机制是怎么实现的?

事务传播机制看似神奇,实际上是使用简单的 ThreadLocal 的机制实现的。所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。这不同于我们以前讲到的透传,Spring并没有做这样的处理。

参考: 你的也是我的。3例ko多线程,局部变量透传

所以事务传播机制,只有翻一遍源代码,才能印象深刻。仅靠文字去传播,很多东西会变得不可描述。

b6vu2ie.png!web

如图,PlatformTransactionManager只有简单的三个抽象接口,定义了包含JDBC在内的Spring所有的事务操作。

Q73UB3z.png!web

我们平常说的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<TransactionInfo> 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);
	}
}

用我做过多解释了吧,一切明显的逻辑,都在代码里。 事务就是在这里创建的。

4、查询方法可以不开启事务么?

事务有个 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查询之前,数据被其他用户改变,就会造成数据的前后不一。

也仅有在这种情况下,要开启读事务。

5、private方法加事务注解有用么?

@Transaction 注解加在 private 上,并没有什么卵用。

这倒不是事务处理的代码去写的特性。由于事务的这些功能,是通过 AOP 方式强加进去的,所以它收到动态代理的控制。

privatefinal 修饰的方法,不会被代理。

但是,你却可以把private方法放在带有事务功能的public方法里。这样,它 看起来 也有了事务的一些功能特性,但它并没有。

End

互联网中,用到的事务并不多,很多都是非常小、速度非常快的接口,对于开发人员来说,事务是个累赘。

但在一些带有金融属性的业务中,或者一些企业级开发应用中,事务确实一个绕不过的坎。一旦深入其中,就会发现这个知识点,露着血盆大口,等君入瓮。

xjjdog 从源码层次,聊到了几个面试常问的问题。不要觉得奇怪,有的人确实一直在拿着脏读、幻读这样的名词来面试。

而这些东西,都属于当时看了恍然大悟,第二天就继续懵逼的内容。

什么时候,才能务实一点呢?

近期热门文章

996的乐趣,你是无法想象的

魔幻现实主义,关爱神经衰弱

一切荒诞的傲慢,皆来源于认知

不要被标题给骗了,画面感十足的消遣文章

《必看!java后端,亮剑诛仙》

后端技术索引,中肯火爆。全网转载上百次。

《学完这100多技术,能当架构师么?(非广告)》

精准点评100多框架,帮你选型

作者简介: 小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流

EnUneen.gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK