在最近做的一個專案裡面,涉及到多資料來源的操作,比較特殊的是,這多個資料庫的表結構完全相同,由於我們使用的ibatis框架作為持久化層,為了防止每一個資料來源都配置一套規則,所以重新實現了資料來源,根據執行緒變數中指定的資料庫連線名稱來獲取實際的資料來源。
一個簡單的實現如下:
public class ProxyDataSource implements DataSource { /** 資料來源池配置 */ private Map<String, DataSource> dataSourcePoolConfig; public Connection getConnection() throws SQLException { return createDataSource().getConnection(); } private synchronized DataSource createDataSource() { String dbName = DataSourceContextHolder.getDbName(); return dataSourcePoolConfig.get(dbName); }
每次呼叫spring事務管理器之前,設定DataSourceContextHolder.set(“dbName”)
事務提交之後在呼叫 DataSourceContextHolder.clear() 方法即可
但是這樣設計實際使用過程中也會遇到一些典型的問題,這就是在仔細瞭解spring中持久化層的設計之後,才能明白所產生的問題的原因。下面主要總結一下spring 持久化的設計。
Jdbc基本的程式設計模型
由於任何持久化層的封裝實際上都是對java.sql.Connection等相關物件的操作,一個典型的資料操作的流程如下:
但在我們實際使用spring和ibatis的時候,都沒有感覺到上面的流程,其實spring已經對外已經遮蔽了上述的操作,讓我們更關注業務邏輯功能,但是我們有必要了解其實現,以便能夠更好運用和定位問題。
開啟事務:
在開啟事務的時候,我們需要初始化事務上下文資訊,以便在業務完成之後,需要知道事務的狀態,以便進行後續的處理,這個上下文資訊可以儲存在 ThreadLocal裡面,包括是否已經開啟事務,事務的超時時間,隔離級別,傳播級別,是否設定為回滾。這個資訊對應用來說是透明的,但是提供給使用者程式設計介面,以便告知業務結束的時候是提交事務還是回滾事務。
獲取連線
首先來看看spring如何獲取資料庫連線的,對於正常情況來看,獲取連線直接呼叫DataSource.getConnection()就可以了,我們在自己實現的時候也肯定會這麼做,但是需要考慮兩種情況(這裡面先不引入事務的傳播屬性):
1 還沒有獲取過連線,這是第一次獲取連線
2 已經獲取過連線,不是第一次獲取連線,可以複用連線
解決獲取資料庫連線的關鍵問題就是如何判斷是否已經可用的連線,而不需要開啟新的資料庫連線,同時由於資料庫連線需要給後續的業務操作複用,如何保持這個連線,並且透明的傳遞給後續流程。對於一個簡單的實現就是使用執行緒上下文變數ThrealLocal來解決以上兩個問題。
具體的實現是:在獲取資料庫連線的時候,判斷當前執行緒執行緒變數裡面是否已經存在相關連線,如果不存在,就創新一個新的連線,如果存在,就直接獲取其對應的連線。在第一次獲取到資料庫連線的時候,我們還需要做一些特殊處理,就是設定自動提交為false。在業務活動結束的時候在進行提交或者回滾。這個時候就是要呼叫connection.setAutoCommit(false)方法。
執行sql
這一部分和業務邏輯相關,通過對外提供一些程式設計介面,可以讓業務決定業務完成之後如何處理事務,比較簡單的就是設定事務狀態。
提交事務:
在開啟事務的時候,事務上下文資訊已經儲存線上程變數裡面了,可以根據事務上下文的資訊,來決定是否是提交還是回滾。其實就是呼叫資料庫連線Connection.commit 和 Connection.rollback 方法。然後需要清空執行緒變數中的事務上下文資訊。相當於結束了當前的事務。
關閉連線:
關閉連線相對比較簡單,由於當前執行緒變數儲存了連線資訊,只需要獲取連線之後,呼叫connection.close方法即可,接著清空執行緒變數的資料庫連線資訊。
上面幾個流程是一個簡單的事務處理流程,在spring中都有對應的實現,見TransactionTemplate.execute方法。Spring定義了一個TransactionSynchronizationManager物件,裡面儲存了各種執行緒變數資訊,
//儲存了資料來源和其對應連線的對映,value是一個Map結構,其中key為datasource,value為其開啟的連線 private static final ThreadLocal resources //這個暫時用不到,不解釋 private static final ThreadLocal synchronizations //當前事務的名字 private static final ThreadLocal currentTransactionName //是否是隻讀事務以及事務的隔離級別(這個一般我們都用不到,都是預設界別) private static final ThreadLocal currentTransactionReadOnly private static final ThreadLocal currentTransactionIsolationLevel //代表是否是一個實際的事務活動,這個後面將) private static final ThreadLocal actualTransactionActive
在獲取連線的時候,可見DataSourceUtils.doGetConnection()方法,就是從呼叫TransactionSynchronizationManager.getResource(dataSource)獲取連線資訊,如果為空,就直接從呼叫dataSource.getConnection()建立新的連線,後面在呼叫
TransactionSynchronizationManager.bindResource(dataSource,conn)繫結資料來源到執行緒變數,以便後續的執行緒在使用。
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(dataSource.getConnection()); } return conHolder.getConnection(); } logger.debug("Fetching JDBC Connection from DataSource"); Connection con = dataSource.getConnection();
在提交事務的時候,見 DataSourceTransactionManager.doCommit方法,其實就是獲取事務狀態資訊以及連線資訊,呼叫conn.commmit方法,比較簡單。
但是實際上,spring事務管理遠遠比上述複雜,我們沒有考慮以下幾種情況:
1 如果當前操作不需要事務支援,也就是每次執行一次,就自動進行提交。如何在同一個架構裡面相容這兩種情況。比如就是簡單的query操作。
2 一個業務活動跨越多個事務,每個事務的傳播級別配置不一樣。後面會拿一個例子來說明
對於第一個問題,比較好解決,首先就是根據執行緒變數裡面獲取資料來源對應的連線,如果有連線,就複用。如果沒有,就建立連線。在判斷當前是否存在活動的事務上下文,如果存在事務資訊,設定conn.setAutoCommit(false),然後設定執行緒上下文,繫結對應的資料來源。如果不存在事務資訊,就直接返回連線給應用。
這樣就會帶來一個新的問題,就是連線如何進行關閉。根據最開始的分析,在存在事務上下文的情況下,直接從獲取執行緒獲取對應的資料庫連線,然後關閉。在關閉的也需要也進行判斷一下即可。在spring裡面,在事務中獲取連線和關閉連線有一些特殊的處理,主要還是和其jdbc以及orm框架設計相容。在jdbcTemplate,IbatiTemplate每執行一次sql操作,就需要獲取conn,執行sql,關閉conn。如果不存在事務上下文,這樣做沒有任何問題,獲取一次連線,使用完成,然後就是比。但是如果存在事務上下文,每次獲取的conn並不一定是真實的物理連線,所以關閉的時候,也不能直接關閉這資料庫連線。Spring的中定義一個ConnectionHandle物件,這個物件持有一個資料庫連線物件,以及該連線上的引用次數(retain屬性)。每次複用一次就retain++ 操作,沒關閉一次,就執行retain-- 操作,在retain 為0的時候,說明沒有任何連線,就可以進行真實的關閉了。