spring使用ThreadLocal將資源和事務繫結到執行緒上
這篇文章想要解釋Spring為什麼會選擇使用ThreadLocal將資源和事務繫結到執行緒上,這背後有著什麼樣的起因和設計動機,通過分析幫助大家更清晰地認識Spring的執行緒繫結機制。
“原始”的資料訪問寫法
訪問任何帶有事務特性的資源系統,像資料庫,都有著相同的特點:首先你需要獲得一個訪問資源的“管道”,對於資料庫來說,這個所謂的“管道”是JDBC裡的Connection,是Hibernate裡的Session.然後你會通過“管道”下達一系列的讀寫指令,比如資料庫的SQL,最後你會斷開這個“管道”,釋放對這個資源的連線。在Spring裡,用訪問資源的“管道”來指代資源,因此JDBC的Connection和Hibernate的Session都被稱之為“資源”(Resource)(本文會交替使用這兩種稱呼)。另一方面,資源與事務又有著緊密的關係,事務的開啟與提交都是在某個“Resource”上進行的。以Hibernate為例,一種“原始”的資料訪問程式往往會寫成這樣:
- Session session = sessionFactory.openSession();//獲取“資源”
- Transaction tx = null;
- try {
- tx = session.beginTransaction(); //開始事務
- ....
- DomainObject domainObject = session.load(...); //資料訪問操作
- ....
- domainObject.processSomeBusinessLogic();//業務邏輯計算
- ....
-
session.save(domainObject); //另一個數據訪問操作
- ....
- session.save(anotherDomainObject); //又一個數據訪問操作
- ....
- session.commit(); //提交事務
- }
- catch (RuntimeException e) {
- tx.rollback();
- throw e;
- }
- finally {
- session.close(); //釋放資源
- }
上述程式碼的思路很直白:首先獲得資料庫“資源”,然後在該資源上開始一個事務,經過一系列夾雜著業務計算和資料訪問的操作之後,提交事務,釋放資源。
分層帶來的困擾
相信很多人一下就能看出上面程式碼的問題:業務邏輯與資料訪問摻雜在了一起,犯了分層的“忌諱”。一個良好的分層系統往往是這樣實現上述程式碼的:使用Service實現業務邏輯,使用DAO向Service提供資料訪問支援。
某個Service的實現類:
- publicclass MyServiceImpl implements MyService {
- publicvoid processBusiness(){
- //在這裡獲得資源並開啟事務麼?NO!會引入資料訪問的API,“汙染"Service,破壞了分層!
- //Session session = sessionFactory.openSession();
- //session.beginTransaction();
- ....
- DomainObject domainObject = myDao.getDomainObject(...); //資料訪問操作
- ....
- domainObject.processSomeBusinessLogic();//業務邏輯計算
- ....
- myDao.save(domainObject); //另一個數據訪問操作
- ....
- myDao.save(anotherDomainObject); //又一個數據訪問操作
- ....
- }
- ....
- }
某個DAO的Hibernate實現類:
- publicclass MyDaoHibernateImpl implements MyDao {
- publicvoid save(DomainObject domainObject){
- //在這裡獲得資源並開啟事務麼?NO!你怎麼確定這個方法一定是一個獨立的事務
- //而不會是某個事務的一部分呢?比如我們上面的Service。
- //Session session = sessionFactory.openSession();
- //session.beginTransaction();
- ....
- session.save(domainObject);
- }
- ....
- }
矛盾的焦點 從“分層”的角度看,上述方案算是“完美”了,但卻迴避了一個現實的技術問題:如何安置“獲取資源”(也就是session)和“開啟事務”的程式碼呢?像程式碼中註釋的那樣,好像放在哪裡都有問題,看上去像是一個“不可調和”的矛盾。如果要解決這個“不可調和”的矛盾,在技術上需要解決兩個難題:
- 如何“透明”地進行事務定界(Transaction Demarcation)?
- 如何構建一個“上下文”,在事務開始與事務提交時,以及在事務過程中所有資料訪問方法都能“隱式”地得到“同一個資源”(資料庫連線/Hibernate Session)。所謂“隱式”是指不能把同一個資源例項用引數的方式傳給資料訪問方法,否則必然會出現資料訪問層的上層程式碼受到資料訪問專有API汙染的問題(即破獲了分層),而使用全域性變數顯然是不行的,因為全域性變數是唯一的,沒有哪個應用能容忍只使用一個數據庫連線,對於一個使用者請求一個執行緒的多執行緒Web應用環境更是如此。
Spring的解決之道
Spring使用基於AOP的宣告式事務定界解決了第一個問題,而使用基於ThreadLocal的資源與事務執行緒繫結成功地解決了第二個問題。(關於spring的具體實現,可以參考我的另一篇文章:Spring原始碼解析(一) Spring事務控制之Hibernate ,第一個問題所涉及原始碼主要是:
org.springframework.aop.framework.JdkDynamicAopProxy 和 org.springframework.transaction.interceptor.TransactionInterceptor
第二個問題所涉及原始碼主要是:
org.springframework.transaction.support.AbstractPlatformTransactionManager 和 org.springframework.transaction.support.TransactionSynchronizationManager)
本文我們重點關注Spring是如何解決第二個問題的,對於這個問題有兩點需要特別地解釋:
- “上下文”:Spring使用的是“執行緒上下文”,也就是TreadLocal,原因非常簡單,做為一種執行緒作用域變數,它能很好地被“隱式”獲取,即在當前執行緒下可以直接得到該變數(避免了引數傳遞),同時又不會像全域性變數那樣作用域過大且全域性只有一個例項。實際上,從更大的背景上來看,大多數的spring應用為B/S架構的web應用,受servlet執行緒模型的影響,此類web應用都是一個使用者請求到達開啟一個新的執行緒進行處理,在此背景下,spring這種以執行緒作為上下文繫結資源和事務的處理方式無疑是非常合適的。
- “資源與事務的生命週期”:如果只從“執行緒繫結”的字面上理解,很容易讓人誤解為繫結到執行緒上的資源和事務的生命週期與執行緒是等長的,這是錯誤的。實際上,資源和事務的生命週期與執行緒生命週期沒有必然聯絡,只是當資源和事務存在時,它們會以TreadLocal的形式繫結到執行緒上而已。而資源的生命週期與事務的生命週期才是等長的,我們把資源-事務這種生命週期關係稱為:Connection-Per-Transaction 或是 Session-Per-Transaction。
Hibernate自己動手豐衣足食
作為一小段插曲,我們聊聊Hibernate。大概是為滿足對Session-Per-Transaction的普遍需求,Hibernate也實現了自己的Session-Per-Transaction模型,就是大家所熟知的SessionFactory.getCurrentSession(),該方法返回繫結在當前執行緒上session例項,若當前執行緒沒有session例項,建立一個新的例項以ThreadLocal的形式繫結到當前執行緒上,同時,該方法生成的session其實是一個session代理,這個代理會對內部的實際session附加如下動作:
- 對session的資料操作方法進行攔截,確認在執行操作前已經呼叫過begainTranscation()開啟了一個事務,否則會丟擲異常。這一點確保了對session的使用必須總是從建立一個事務開始的。
- 當事務在commit或rollback後session會自動關閉。這一點確保了事務提交後session也將不可用。
一切是這樣進行的
結合上述場景和Spring的解決方案,一個使用了Spring宣告性事務,實現了良好分層的程式,它的資源和事務在Spring的控制下是這樣工作的:
- 若當前執行緒執行到了一個需要進行事務控制的方法(如某個service的方法),通過AOP攔截,spring會在方法執行前申請一個數據庫連線或者一個hibernate session.
- 成功獲得資源後,開啟一個事務。
- 將資源也就是資料庫連線或是hibernate session的例項存放於當前執行緒的ThreadLocal裡(也就是進行所謂的執行緒繫結)
- 在方法執行過程中,任何需要獲得資料庫連線或是hibernate session進行資料訪問的地方都會從當前執行緒的ThreadLocal裡取出同一個資料庫連線或是hibernate session的例項進行操作(這個動作由Spring提供的各種Template類實現)。
- 方法執行結束,同樣通過AOP攔截,spring取出繫結到當前執行緒上的事務(對於hibernate來說就是取出繫結在當前執行緒上一個SessionHolder例項,它儲存著當前的session與transaction例項),執行提交。
- 事務提交之後,釋放資源,清空當前執行緒上繫結的所有物件!
- 如果該執行緒之後有新的事務發起,一切會重新開始,Spring會使用新的資料庫連線或是hibernate session例項,開始新的事務,兩個事務之間沒有任何關係。
一個小小的總結
- Connection-Per-Transaction/Session-Per-Transaction幾乎總是你需要的。
- 在分層架構中,有些變數或物件確實需要跨越分層工作(比如本文示例中的Connection/Session/Transaction),你可能需一種“上下文”(或者說是一種跨層的作用域)來存放這種變數或是物件,從而避免以“引數”的形式在層間傳遞它,執行緒區域性變數ThreadLocal可能正是你需要的.