在最近作的一個項目裏面,涉及到多數據源的操做,比較特殊的是,這多個數據庫的表結構徹底相同,因爲咱們使用的ibatis框架做爲持久化層,爲了防止每個數據源都配置一套規則,因此從新實現了數據源,根據線程變量中指定的數據庫鏈接名稱來獲取實際的數據源。java
一個簡單的實現以下:spring
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」) sql
事務提交以後在調用 DataSourceContextHolder.clear() 方法便可數據庫
可是這樣設計實際使用過程當中也會遇到一些典型的問題,這就是在仔細瞭解spring中持久化層的設計以後,才能明白所產生的問題的緣由。下面主要總結一下spring 持久化的設計。編程
Jdbc基本的編程模型架構
因爲任何持久化層的封裝實際上都是對java.sql.Connection等相關對象的操做,一個典型的數據操做的流程以下:框架
但在咱們實際使用spring和ibatis的時候,都沒有感受到上面的流程,其實spring已經對外已經屏蔽了上述的操做,讓咱們更關注業務邏輯功能,可是咱們有必要了解其實現,以便可以更好運用和定位問題。spa
開啓事務:線程
在開啓事務的時候,咱們須要初始化事務上下文信息,以便在業務完成以後,須要知道事務的狀態,以便進行後續的處理,這個上下文信息能夠保存在 ThreadLocal裏面,包括是否已經開啓事務,事務的超時時間,隔離級別,傳播級別,是否設置爲回滾。這個信息對應用來講是透明的,可是提供給使用者編程接口,以便告知業務結束的時候是提交事務仍是回滾事務。debug
獲取鏈接
首先來看看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的時候,說明沒有任何鏈接,就能夠進行真實的關閉了。