天天看點

Spring 事務管理進階應用難點剖析: 第 1 部分

Spring 的事務管理是被使用得最多的功能之一,雖然 Spring 事務管理已經幫助程式員将要做的事情減到了最小。但在實際開發中,如果使用不當,依然會造成資料連接配接洩漏等問題。本系列以實際應用中所碰到的各種複雜的場 景為着眼點,對這些應用的難點進行深度的剖析。

很少有使用 Spring 但不使用 Spring 事務管理器的應用,是以常常有人會問:是否用了 Spring,就一定要用 Spring 事務管理器,否則就無法進行資料的持久化操作呢?事務管理器和 DAO 是什麼關系呢?

也 許是 DAO 和事務管理如影随行的緣故吧,這個看似簡單的問題實實在在地存在着,從初學者心中湧出,萦繞在開發老手的腦際。答案當然是否定的!我們都知道:事務管理是 保證資料操作的事務性(即原子性、一緻性、隔離性、持久性,也即所謂的 ACID),脫離了事務性,DAO 照樣可以順利地進行資料的操作。

下面,我們來看一段使用 Spring JDBC 進行資料通路的代碼:

jdbcWithoutTransManager.xml 的配置檔案如下所示:

運作 UserJdbcWithoutTransManagerService,在控制台上打出如下的結果:

在 jdbcWithoutTransManager.xml 中,沒有配置任何事務管理器,但是資料已經成功持久化到資料庫中。在預設情況下,dataSource 資料源的 autoCommit 被設定為 true ―― 這也意謂着所有通過 JdbcTemplate 執行的語句馬上送出,沒有事務。如果将 dataSource 的 defaultAutoCommit 設定為 false,再次運作 UserJdbcWithoutTransManagerService,将抛出錯誤,原因是新增及更改資料的操作都沒有送出到資料庫,是以 ④ 處的語句因無法從資料庫中查詢到比對的記錄而引發異常。

對于強調讀速度的應用,資料庫本身可能就不支援事務,如使用 MyISAM 引擎的 MySQL 資料庫。這時,無須在 Spring 應用中配置事務管理器,因為即使配置了,也是沒有實際用處的。

不 過,對于 Hibernate 來說,情況就有點複雜了。因為 Hibernate 的事務管理擁有其自身的意義,它和 Hibernate 一級緩存有密切的關系:當我們調用 Session 的 save、update 等方法時,Hibernate 并不直接向資料庫發送 SQL 語句,而是在送出事務(commit)或 flush 一級緩存時才真正向資料庫發送 SQL。是以,即使底層資料庫不支援事務,Hibernate 的事務管理也是有一定好處的,不會對資料操作的效率造成負面影響。是以,如果是使用 Hibernate 資料通路技術,沒有理由不配置 HibernateTransactionManager 事務管理器。

但是,不使用 Hibernate 事務管理器,在 Spring 中,Hibernate 照樣也可以工作,來看下面的例子:

此時,采用 hiberWithoutTransManager.xml 的配置檔案,其配置内容如下:

運作 UserHibernateWithoutTransManagerService,程式正确執行,并得到類似于 UserJdbcWithoutTransManagerService 的執行結果,這說明 Hibernate 在 Spring 中,在沒有事務管理器的情況下,依然可以正常地進行資料的通路。

<a href="http://www.ibm.com/developerworks/cn/java/j-lo-spring-ts1/index.html#ibm-pcon">回頁首</a>

Web、 Service 及 DAO 三層劃分就像西方國家的立法、行政、司法三權分立一樣被奉為金科玉律,甚至有開發人員認為如果要使用 Spring 的事務管理就一定先要進行三層的劃分。這個看似荒唐的論調在開發人員中頗有市場。更有甚者,認為每層必須先定義一個接口,然後再定義一個實作類。其結果 是:一個很簡單的功能,也至少需要 3 個接口,3 個類,再加上視圖層的 JSP 和 JS 等,打牌都可以轉上兩桌了,這種誤解贻害不淺。

對 将“面向接口程式設計”奉為圭臬,認為放之四海而皆準的論調,筆者深不以為然。是的,“面向接口程式設計”是 Martin Fowler,Rod Johnson 這些大師提倡的行事原則。如果拿這條原則去開發架構,開發産品,怎麼強調都不為過。但是,對于我們一般的開發人員來說,做的最多的是普通工程項目,往往最 多的隻是一些對資料庫增、删、查、改的功能。此時,“面向接口程式設計”除了帶來更多的類檔案外,看不到更多其它的好處。

Spring 架構提供的所有附加的好處(AOP、注解增強、注解 MVC 等)唯一的前提就是讓 POJO 的類變成一個受 Spring 容器管理的 Bean,除此以外沒有其它任何的要求。下面的執行個體用一個 POJO 完成所有的功能,既是 Controller,又是 Service,還是 DAO:

通過 @Controller 注解将 MixLayerUserService 變成 Web 層的 Controller,同時也是 Service 層的服務類。此外,由于直接使用 JdbcTemplate 通路資料,是以 MixLayerUserService 還是一個 DAO。來看一下對應的 Spring 配置檔案:

在 ① 處,我們定義配置了 AnnotationMethodHandlerAdapter,以便啟用 Spring MVC 的注解驅動功能。而②和③處通過 Spring 的 aop 及 tx 命名空間,以及 Aspject 的切點表達式文法進行事務增強的定義,對 MixLayerUserService 的所有公有方法進行事務增強。要使程式能夠運作起來還必須進行 web.xml 的相關配置:

這個配置檔案很簡單,唯一需要注意的是 DispatcherServlet 的配置。預設情況下 Spring MVC 根據 Servlet 的名字查找 WEB-INF 下的 &lt;servletName&gt;-servlet.xml 作為 Spring MVC 的配置檔案,在此,我們通過 contextConfigLocation 參數顯式指定 Spring MVC 配置檔案的确切位置。

将 org.springframework.jdbc 及 org.springframework.transaction 的日志級别設定為 DEBUG,啟動項目,并通路 http://localhost:8088/logon.do?userName=tom 應用,MixLayerUserService#logon 方法将作出響應,檢視背景輸出日志:

日志中粗體部分說明了 MixLayerUserService#logon 方法已經正确運作在事務上下文中。

Spring 架構本身不應該是複雜化代碼的理由,使用 Spring 的開發者應該是無拘無束的:從實際應用出發,去除掉那些所謂原則性的接口,去除掉強制分層的束縛,簡單才是硬道理。

Spring 事務一個被訛傳很廣說法是:一個事務方法不應該調用另一個事務方法,否則将産生兩個事務。結果造成開發人員在設計事務方法時束手束腳,生怕一不小心就踩到地雷。

其實這種是不認識 Spring 事務傳播機制而造成的誤解,Spring 對事務控制的支援統一在 TransactionDefinition 類中描述,該類有以下幾個重要的接口方法:

int getPropagationBehavior():事務的傳播行為

int getIsolationLevel():事務的隔離級别

int getTimeout():事務的過期時間

boolean isReadOnly():事務的讀寫特性。

很 明顯,除了事務的傳播行為外,事務的其它特性 Spring 是借助底層資源的功能來完成的,Spring 無非隻充當個代理的角色。但是事務的傳播行為卻是 Spring 憑借自身的架構提供的功能,是 Spring 提供給開發者最珍貴的禮物,訛傳的說法玷污了 Spring 事務架構最美麗的光環。

所謂事務傳播行為就是多個事務方法互相調用時,事務如何在這些方法間傳播。Spring 支援 7 種事務傳播行為:

PROPAGATION_REQUIRED 如果目前沒有事務,就建立一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。

PROPAGATION_SUPPORTS 支援目前事務,如果目前沒有事務,就以非事務方式執行。

PROPAGATION_MANDATORY 使用目前的事務,如果目前沒有事務,就抛出異常。

PROPAGATION_REQUIRES_NEW 建立事務,如果目前存在事務,把目前事務挂起。

PROPAGATION_NOT_SUPPORTED 以非事務方式執行操作,如果目前存在事務,就把目前事務挂起。

PROPAGATION_NEVER 以非事務方式執行,如果目前存在事務,則抛出異常。

PROPAGATION_NESTED 如果目前存在事務,則在嵌套事務内執行。如果目前沒有事務,則執行與 PROPAGATION_REQUIRED 類似的操作。

Spring 預設的事務傳播行為是 PROPAGATION_REQUIRED,它适合于絕大多數的情況。假設 ServiveX#methodX() 都工作在事務環境下(即都被 Spring 事務增強了),假設程式中存在如下的調用 鍊:Service1#method1()-&gt;Service2#method2()-&gt;Service3#method3(),那麼這 3 個服務類的 3 個方法通過 Spring 的事務傳播機制都工作在同一個事務中。

下面,我們來看一下實 例,UserService#logon() 方法内部調用了 UserService#updateLastLogonTime() 和 ScoreService#addScore() 方法,這兩個類都繼承于 BaseService。它們之間的類結構說明如下:

Spring 事務管理進階應用難點剖析: 第 1 部分

具體的代碼如下所示:

UserService 中注入了 ScoreService 的 Bean,ScoreService 的代碼如下所示:

通過 Spring 的事務配置為 ScoreService 及 UserService 中所有公有方法都添加事務增強,讓這些方法都工作于事務環境下。下面是關鍵的配置代碼:

将日志級别設定為 DEBUG,啟動 Spring 容器并執行 UserService#logon() 的方法,仔細觀察如下的輸出日志:

從 上面的輸入日志中,可以清楚地看到 Spring 為 UserService#logon() 方法啟動了一個新的事務,而 UserSerive#updateLastLogonTime() 和 UserService#logon() 是在相同的類中,沒有觀察到有事務傳播行為的發生,其代碼塊好像“直接合并”到 UserService#logon() 中。接着,當執行到 ScoreService#addScore() 方法時,我們就觀察到了發生了事務傳播的行為:Participating in existing transaction,這說明 ScoreService#addScore() 添加到 UserService#logon() 的事務上下文中,兩者共享同一個事務。是以最終的結果是 UserService 的 logon(), updateLastLogonTime() 以及 ScoreService 的 addScore 都工作于同一事務中。

由于 Spring 的事務管理器是通過線程相關的 ThreadLocal 來儲存資料通路基礎設施,再結合 IOC 和 AOP 實作進階聲明式事務的功能,是以 Spring 的事務天然地和線程有着千絲萬縷的聯系。

我 們知道 Web 容器本身就是多線程的,Web 容器為一個 Http 請求建立一個獨立的線程,是以由此請求所牽涉到的 Spring 容器中的 Bean 也是運作于多線程的環境下。在絕大多數情況下,Spring 的 Bean 都是單執行個體的(singleton),單執行個體 Bean 的最大的好處是線程無關性,不存在多線程并發通路的問題,也即是線程安全的。

一個類能夠以單執行個體的方式運作的前提是“無狀态”:即一個類不 能擁有狀态化的成員變量。我們知道,在傳統的程式設計中,DAO 必須執有一個 Connection,而 Connection 即是狀态化的對象。是以傳統的 DAO 不能做成單執行個體的,每次要用時都必須 new 一個新的執行個體。傳統的 Service 由于将有狀态的 DAO 作為成員變量,是以傳統的 Service 本身也是有狀态的。

但是在 Spring 中,DAO 和 Service 都以單執行個體的方式存在。Spring 是通過 ThreadLocal 将有狀态的變量(如 Connection 等)本地線程化,達到另一個層面上的“線程無關”,進而實作線程安全。Spring 不遺餘力地将狀态化的對象無狀态化,就是要達到單執行個體化 Bean 的目的。

由于 Spring 已經通過 ThreadLocal 的設施将 Bean 無狀态化,是以 Spring 中單執行個體 Bean 對線程安全問題擁有了一種天生的免疫能力。不但單執行個體的 Service 可以成功運作于多線程環境中,Service 本身還可以自由地啟動獨立線程以執行其它的 Service。下面,通過一個執行個體對此進行描述:

将日志級别設定為 DEBUG,執行 UserService#logon() 方法,觀察以下輸出的日志:

在 ① 處,在主線程(main)執行的 UserService#logon() 方法的事務啟動,在 ③ 處,其對應的事務送出,而在子線程(Thread-2)執行的 ScoreService#addScore() 方法的事務在 ② 處啟動,在 ④ 處對應的事務送出。

是以,我們可以得出這樣的結論:在 相同線程中進行互相嵌套調用的事務方法工作于相同的事務中。如果這些互相嵌套調用的方法工作在不同的線程中,不同線程下的事務方法工作在獨立的事務中。

Spring 聲明式事務是 Spring 最核心,最常用的功能。由于 Spring 通過 IOC 和 AOP 的功能非常透明地實作了聲明式事務的功能,一般的開發者基本上無須了解 Spring 聲明式事務的内部細節,僅需要懂得如何配置就可以了。

但 是在實際應用開發過程中,Spring 的這種透明的高階封裝在帶來便利的同時,也給我們帶來了迷惑。就像通過流言傳播的消息,最終聽衆已經不清楚事情的真相了,而這對于應用開發來說是很危險 的。本系列文章通過剖析實際應用中給開發者造成迷惑的各種難點,通過分析 Spring 事務管理的内部運作機制将真相還原出來。

在本文中,我們通過剖析了解到以下的真相:

在沒有事務管理的情況下,DAO 照樣可以順利進行資料操作;

将應用分成 Web,Service 及 DAO 層隻是一種參考的開發模式,并非是事務管理工作的前提條件;

Spring 通過事務傳播機制可以很好地應對事務方法嵌套調用的情況,開發者無須為了事務管理而刻意改變服務方法的設計;

由于單執行個體的對象不存線上程安全問題,是以進行事務管理增強的 Bean 可以很好地工作在多線程環境下。

混合使用多種資料通路技術(如 Spring JDBC + Hibernate)的事務管理問題;

進行 Spring AOP 增強的 Bean 存在哪些特殊的情況。