Spring數據訪問和事務


  • 1、模型
  • 2、解耦
  • 3、實現
    • 3.1 核心接口
    • 3.2 代碼分析
      • 3.2.1 事務管理
      • 3.2.2 數據訪問
  • 4、使用
    • 4.1 編程模式
    • 4.2 配置模式
      • 4.2.1 聲明式配置方式
      • 4.2.2 注解式配置方式
  • 5、總結

1、模型

在一般的編程習慣中,Spring的數據訪問和事務處理的層次結構歸納如下圖所示:

                                                          圖. 1

2、解耦

Spring事務作為一個獨立的組件,其目的就是為了與數據訪問組件進行分離,這也是Spring事務框架設計的原則。根據這一職責清晰的原則,Spring在設計時就對事務和數據訪問進行了很好的職責划分,這個可以從spring-tx和spring-jdbc這兩個包就可以看出來。

但是在實際的代碼中,會遇到一個棘手的問題:事務和數據訪問操作都需要同一個數據庫連接資源,那么它們之間怎么傳遞呢?

這里涉及三個方面:一是線程安全,二是資源的唯一性,三是事務和數據訪問的解耦。

                                                                                     圖. 2

在圖2中的1、2和3這三個地方都需要使用數據庫連接,並且是同一個連接。Spring的做法是將該連接放在一個統一的地方,要使用該資源,都從這個地方獲取,這樣就解決了事務模塊和數據訪問模塊之間的緊耦合。

解除耦合之后,對於不同的ORM技術,則需要提供不同的事務管理實現,如下圖所示:

                                                                              圖. 3

3、實現

3.1 核心接口

Spring事務框架的核心接口是:TransactionDefinition,TransactionStatus和PlatformTransactionManager。

TransactionDefinition用於定義事務屬性,包括事務的隔離級別,事務的傳播行為,事務的超時時間和是否為只讀事務。對於隔離級別和傳播行為這里就不深入了,可以網上找到很多資料。事務的超時時間就是一個事務需要在規定的時間里完成。只讀事務表示在一個事務里不允許進行寫操作。

TransactionStatus表示整個事務處理過程中的事務狀態,通過事務狀態可以進行事務的相應操作。

PlatformTransactionManager是對整個事務行為的抽象,定義了一個完整事務過程中的相關操作。對於不同的ORM技術,需要有不同的實現。

3.2 代碼分析

下面簡單分析一下事務和數據訪問之間數據庫連接資源是如何傳遞的,以JdbcTemplate為例,代碼如下:

3.2.1 事務管理

                                                             圖. 4

代碼1:事務管理器首先獲取事務對象(具體步驟請看代碼2),然后根據事務對象判斷是否已經存在事務,如果已經存在事務,則根據傳播特性做進一步處理,這里就不介紹了;如果不存在事務,則開啟一個新事務,開啟新事務的具體內容見代碼3。

 1 //************************************** 代碼1: AbstractPlatformTransactionManager.java ****************************
 2 public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
 3    Object transaction = doGetTransaction();                                                   //獲取事務
 4    ......
 5    if (isExistingTransaction(transaction)) {                                                  //如果已經存在事務,則根據傳播特性再進一步處理
 6       // Existing transaction found -> check propagation behavior to find out how to behave.
 7       return handleExistingTransaction(definition, transaction, debugEnabled);
 8    }
 9  
10    // No existing transaction found -> check propagation behavior to find out how to proceed.
11    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
12       throw new IllegalTransactionStateException(
13             "No existing transaction found for transaction marked with propagation 'mandatory'");
14    }
15    else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||  //如果不存在事務,則開啟一個新事務
16          definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
17       definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
18       SuspendedResourcesHolder suspendedResources = suspend(null);
19        
20       try {
21          boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
22          DefaultTransactionStatus status = newTransactionStatus(
23                definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
24          doBegin(transaction, definition);                                                      //開啟事務
25          prepareSynchronization(status, definition);
26          return status;
27       }
28       ......
29    }
30    ......
31 }
View Code

代碼2:獲取事務對象,事務對象中包含了一個數據庫連接,這個數據庫連接是從事務同步管理器中獲取的。事務同步管理器管理了一個ThreadLocal變量,它用於存放當前線程使用的數據連接資源。

//***************************************** 代碼2: DataSourceTransactionManager.java *****************************************
@Override
protected Object doGetTransaction() {
   DataSourceTransactionObject txObject = new DataSourceTransactionObject();
   txObject.setSavepointAllowed(isNestedTransactionAllowed());
   ConnectionHolder conHolder =
      (ConnectionHolder) TransactionSynchronizationManager.getResource(this.dataSource);  //通過事務同步管理器獲取連接,從當前線程的ThreadLocal中獲取
   txObject.setConnectionHolder(conHolder, false);
   return txObject;
}
View Code

代碼3:開啟事務,從事務對象中獲取連接,如果連接不存在則從數據庫連接池中獲取新的連接,關閉自動提交,設定事務超時時間,最后將數據庫連接存儲在事務同步管理中,即綁定在當前線程上。

//***************************************** 代碼3: DataSourceTransactionManager.java *****************************************
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
   DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
   Connection con = null;
 
   try {
      if (txObject.getConnectionHolder() == null ||                                  //如果事務對象里沒有連接,則從數據源(連接池)中獲取新的連接
            txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
         Connection newCon = this.dataSource.getConnection();
         if (logger.isDebugEnabled()) {
            logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
         }
         txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
      }
 
      txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
      con = txObject.getConnectionHolder().getConnection();
 
      Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
      txObject.setPreviousIsolationLevel(previousIsolationLevel);
 
      // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
      // so we don't want to do it unnecessarily (for example if we've explicitly
      // configured the connection pool to set it already).
      if (con.getAutoCommit()) {
         txObject.setMustRestoreAutoCommit(true);
         if (logger.isDebugEnabled()) {
            logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
         }
         con.setAutoCommit(false);
      }
      txObject.getConnectionHolder().setTransactionActive(true);
 
      int timeout = determineTimeout(definition);
      if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
         txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
      }
 
      // Bind the session holder to the thread.
      if (txObject.isNewConnectionHolder()) {
         TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder()); //將連接綁定到ThreadLocal中
      }
   }
 
   catch (Throwable ex) {
      DataSourceUtils.releaseConnection(con, this.dataSource);
      throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
   }
}
View Code

3.2.2 數據訪問

                                                           圖. 5

代碼4:數據訪問,先獲取數據庫連接:a、從事務同步管理器中獲取連接,如果當前數據訪問在一個事務中,那么一定可以獲得一個連接;b、如果當前數據方面沒有在一個事務中,那么將從數據庫連接池中獲取新的連接。

//***************************************** 代碼4: JdbcTemplate.java *****************************************
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
   Assert.notNull(action, "Callback object must not be null");
 
   Connection con = DataSourceUtils.getConnection(getDataSource());
   ......
}
  
//***************************************** DataSourceUtils.java *********************************************
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
   Assert.notNull(dataSource, "No DataSource specified");
 
   ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); //先從ThreadLocal中獲取連接
   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();
   }
   // Else we either got no holder or an empty thread-bound holder here.
 
   logger.debug("Fetching JDBC Connection from DataSource");
   Connection con = dataSource.getConnection();                                                       //如果ThreadLocal中沒有連接,則從數據源(連接池)中獲取新的連接
   ......
}
View Code

4、使用

對於Spring事務的使用最原始的是使用編程模式直接操作事務,這樣可以控制事務的所有行為;另一個是我們都熟悉的配置模式。

4.1 編程模式

編程模式使用示例,以JdbcTemplate為例:

首先需要進行適當的配置,從這些配置中間我們可以得到清晰的脈絡,從數據源到ORM,再到事務。從另一個角度可以看到,ORM和事務管理器的配置都依賴了數據源,這也說明它們之間在數據庫連接上存在不尋常的關系,而這一點已經在上文中進行了講解。

1. 配置數據源(包括數據庫驅動和連接池)

<bean id="meilvDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${driver}"/>
<property name="jdbcUrl" value="${meilv_url}"/>
<property name="user" value="${meilv_username}"/>
<property name="password" value="${meilv_password}"/>
</bean>

2. 配置數據訪問方式(ORM)

<!-- jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="meilvDataSource"/>
</bean>

3. 配置事務管理器

<!-- 數據庫的事務管理器配置 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="meilvDataSource"/>
</bean>

4. 編碼

然后就是我們自己編寫代碼了:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class TransactionThreadTest {
 
    private static final ExecutorService executor = Executors.newFixedThreadPool(1);
 
    @Autowired
    private PlatformTransactionManager transactionManager;
 
    @Autowired
    private IUserDAO userDAO;
 
    @Test
    public void testTransactionThread() {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        //def.setReadOnly(true);
        Boolean flag = def.isReadOnly();
        TransactionStatus status = transactionManager.getTransaction(def);
 
        try {
 
            // 更新庫存
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        syncDisneyPluStock();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            executor.execute(runnable);
 
            Thread.sleep(1000);
 
            UserDO userDO = new UserDO();
            userDO.setName("lanlan");
            userDAO.insert(userDO);
 
        } catch (Exception e) {
            transactionManager.rollback(status);
        }
        transactionManager.commit(status);
    }
 
    public void syncDisneyPluStock() throws Exception {
        UserDO userDO = new UserDO();
        userDO.setName("dd");
        userDAO.insert(userDO);
        //throw new Exception();
    }
}
View Code

對於編程式事務管理,缺點是事務管理代碼和業務邏輯代碼相互混雜,並且所有代碼都要自己寫,異常也都要自己處理,這些都是很繁瑣的事情。針對這個情況,Spring提供了TransactionTemplate的編程方式,這種模板方法設計模式統一處理了事務的流程,用戶只需要關注自己的特定操作就可以了,具體通過實現相關的回調接口來完成。

順便提一句:這種模式跟JDBCTemplate的編程方式是一樣的,兩者雖然在不同模塊內,但是設計模式確是相同的。

4.2 配置模式

Spring的事務配置有多種方式,最常見的有XML中的聲明式配置方式,還有注解式的配置方式。

4.2.1 聲明式配置方式

Spring的事務最終還是通過使用AOP來實現的。聲明式配置方式中,通過<aop:pointcut>定義切點,通過<tx:advice>定義通知,通過<aop:advisor>將這兩者聯系起來形成切面。

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="meilvDataSource"/>
</bean>
 
<!-- 使用tx/aop來配置 -->
<aop:config>
    <!-- 通過aop定義事務增強切面 -->
    <aop:pointcut id="serviceMethod" expression="execution(* com.test.aop.*.*(..))" />
    <!-- 引用事務增強 -->
    <aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice" />
</aop:config>
 
<!--事務增強 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!-- 事務屬性定義 -->
    <tx:attributes>
        <tx:method name="action" read-only="false" />
        <tx:method name="work" rollback-for="RuntimeException" />
    </tx:attributes>
</tx:advice>
View Code

4.2.2 注解式配置方式

對於注解式配置方式,只需要在有事務的方法上增加事務注解就會生效。

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="meilvDataSource"/>
</bean>
  
<tx:annotation-driven transaction-manager="transactionManager"/>
View Code

在類或者方法上使用注解@Transactional就可以進行事務管理了。

5、總結

Spring的事務管理和數據訪問模塊職責相當清晰,認識這一設計原則對我們學習他們具有根本性的作用,本文試圖理清兩者之間的關系,而這所謂的“關系”就是數據庫連接,這也是Spring事務和數據訪問都需要依賴的基礎。

從另一方面來看,Spring對這兩者關系的處理很值得我們學習和借鑒,不管是多線程編程的線程安全性,還是模塊之間的解耦。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM