再深一點:面試工做兩不誤,源碼級理解Spring事務

原創:小姐姐味道(微信公衆號ID:xjjdog),歡迎分享,轉載請保留出處。java

Spring有5種隔離級別,7種傳播行爲。這是面試常問的內容,也是代碼中常常碰到的知識點。這些知識枯燥並且乏味,其中有些很是的繞。若是栽在這上面,就實在是太惋惜了。程序員

xjjdog在一些事務的基礎上,再探討幾個容易淡忘的概念,從源碼層面找緣由,加深咱們的理解,問題大概包括:面試

  1. Spring的事務和數據庫的事務隔離是一個概念麼?
  2. Spring是如何實現事務的?
  3. 事務隔離機制都有哪些?
  4. 事務傳播機制都有哪些?
  5. 查詢語句須要開事務麼?
  6. private方法加事務註解有用麼?

一、Spring的事務和數據庫的事務隔離是一個概念麼?

先來第一個問題,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引擎)。併發

咱們會使用以下的方式進行聲明。若是不是有性能和需求問題,就不要瞎改。事務處理弄很差是會鎖表的,而鎖表在大併發的狀況下是會死人的。ide

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
複製代碼

二、Spring事務的7種傳播機制

只要寫代碼,代碼總會存在嵌套,或者循環,形成了事務的嵌套或者循環。那麼事務在這些狀況下,根據配置會有不一樣的反應。高併發

  • REQUIRED 這是默認的。表示當前方法必須在一個具備事務的上下文中運行,若有客戶端有事務在進行,那麼被調用端將在該事務中運行,不然的話從新開啓一個事務。(若是被調用端發生異常,那麼調用端和被調用端事務都將回滾)
  • REQUIRE_NEW 表示當前方法必須運行在它本身的事務中。若是存在當前事務,在該方法執行期間,當前事務會被掛起
  • NESTED 若是當前方法正有一個事務在運行中,則該方法應該運行在一個嵌套事務中,被嵌套的事務能夠獨立於被封裝的事務中進行提交或者回滾。若是封裝事務存在,而且外層事務拋出異常回滾,那麼內層事務必須回滾,反之,內層事務並不影響外層事務。若是封裝事務不存在,則同required的同樣
  • SUPPORTS 表示當前方法沒必要須要具備一個事務上下文,可是若是有一個事務的話,它也能夠在這個事務中運行
  • NOT_SUPPORTED 表示該方法不該該在一個事務中運行。若是有一個事務正在運行,他將在運行期被掛起,直到這個事務提交或者回滾才恢復執行
  • MANDATORY 表示當前方法必須在一個事務中運行,若是沒有事務,將拋出異常
  • NEVER 表示當方法務不該該在一個事務中運行,若是存在一個事務,則拋出異常

通常用得比較多的是REQUIREDREQUIRES_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<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);
	}
}
複製代碼

不用我作過多解釋了吧,一切明顯的邏輯,都在代碼裏。事務就是在這裏建立的。

四、查詢方法能夠不開啓事務麼?

事務有個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查詢以前,數據被其餘用戶改變,就會形成數據的先後不一。

也僅有在這種狀況下,要開啓讀事務。

五、private方法加事務註解有用麼?

@Transaction註解加在private上,並無什麼卵用。

這倒不是事務處理的代碼去寫的特性。因爲事務的這些功能,是經過AOP方式強加進去的,因此它收到動態代理的控制。

privatefinal修飾的方法,不會被代理。

可是,你卻能夠把private方法放在帶有事務功能的public方法裏。這樣,它看起來也有了事務的一些功能特性,但它並無。

End

互聯網中,用到的事務並很少,不少都是很是小、速度很是快的接口,對於開發人員來講,事務是個累贅。

但在一些帶有金融屬性的業務中,或者一些企業級開發應用中,事務確實一個繞不過的坎。一旦深刻其中,就會發現這個知識點,露着血盆大口,等君入甕。

xjjdog從源碼層次,聊到了幾個面試常問的問題。不要以爲奇怪,有的人確實一直在拿着髒讀、幻讀這樣的名詞來面試。

而這些東西,都屬於當時看了恍然大悟,次日就繼續懵逼的內容。

何時,才能務實一點呢?

做者簡介:小姐姐味道 (xjjdog),一個不容許程序員走彎路的公衆號。聚焦基礎架構和Linux。十年架構,日百億流量,與你探討高併發世界,給你不同的味道。個人我的微信xjjdog0,歡迎添加好友,​進一步交流。​

相關文章
相關標籤/搜索