1. 程式人生 > >Spring事務超時時間可能存在的錯誤認識

Spring事務超時時間可能存在的錯誤認識

1、先看程式碼

1.1、spring-config.xml

Java程式碼  收藏程式碼
  1. <bean id="dataSource"class="org.springframework.jdbc.datasource.DriverManagerDataSource">  
  2.     <property name="driverClassName" value="com.mysql.jdbc.Driver"/>  
  3.     <property name="url" value="jdbc:mysql://localhost:3306/test?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=utf-8"
    />  
  4.     <property name="username" value="root"/>  
  5.     <property name="password" value=""/>  
  6. </bean>  
  7. <bean id="txManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
  8.     <property name="dataSource" ref="dataSource"/>  
  9. </bean>  

1.2、測試用例

Java程式碼  收藏程式碼
  1. @RunWith(SpringJUnit4ClassRunner.class)  
  2. @ContextConfiguration(locations = "classpath:spring-config.xml")  
  3. @TransactionConfiguration(transactionManager = "txManager", defaultRollback = false)  
  4. @Transactional(timeout = 2)  
  5. publicclass Timeout1Test {  
  6.     @Autowired
  7.     private DataSource ds;  
  8.     @Test
  9.     publicvoid testTimeout() throws InterruptedException {  
  10.         System.out.println(System.currentTimeMillis());  
  11.         JdbcTemplate jdbcTemplate = new JdbcTemplate(ds);  
  12.         jdbcTemplate.execute(" update test set name = name || '1'");  
  13.         System.out.println(System.currentTimeMillis());  
  14.         Thread.sleep(3000L);  
  15.     }  
  16. }  

我設定事務超時時間是2秒;但我事務肯定執行3秒以上;為什麼沒有起作用呢?  這其實是對Spring實現的事務超時的錯誤認識。那首先分析下Spring事務超時實現吧。

2、分析

2.1、在此我們分析下DataSourceTransactionManager;首先開啟事物會呼叫其doBegin方法:

Java程式碼  收藏程式碼
  1. …………  
  2. int timeout = determineTimeout(definition);  
  3. if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {  
  4.     txObject.getConnectionHolder().setTimeoutInSeconds(timeout);  
  5. }  
  6. …………  

 其中determineTimeout用來獲取我們設定的事務超時時間;然後設定到ConnectionHolder物件上(其是ResourceHolder子類),接著看ResourceHolderSupport的setTimeoutInSeconds實現:

Java程式碼  收藏程式碼
  1. publicvoid setTimeoutInSeconds(int seconds) {  
  2.     setTimeoutInMillis(seconds * 1000);  
  3. }  
  4. publicvoid setTimeoutInMillis(long millis) {  
  5.     this.deadline = new Date(System.currentTimeMillis() + millis);  
  6. }  

大家可以看到,其會設定一個deadline時間;用來判斷事務超時時間的;那什麼時候呼叫呢?首先檢查該類中的程式碼,會發現:

Java程式碼  收藏程式碼
  1. publicint getTimeToLiveInSeconds() {  
  2.     double diff = ((double) getTimeToLiveInMillis()) / 1000;  
  3.     int secs = (int) Math.ceil(diff);  
  4.     checkTransactionTimeout(secs <= 0);  
  5.     return secs;  
  6. }  
  7. publiclong getTimeToLiveInMillis() throws TransactionTimedOutException{  
  8.     if (this.deadline == null) {  
  9.         thrownew IllegalStateException("No timeout specified for this resource holder");  
  10.     }  
  11.     long timeToLive = this.deadline.getTime() - System.currentTimeMillis();  
  12.     checkTransactionTimeout(timeToLive <= 0);  
  13.     return timeToLive;  
  14. }  
  15. privatevoid checkTransactionTimeout(boolean deadlineReached) throws TransactionTimedOutException {  
  16.     if (deadlineReached) {  
  17.         setRollbackOnly();  
  18.         thrownew TransactionTimedOutException("Transaction timed out: deadline was " + this.deadline);  
  19.     }  
  20. }  

會發現在呼叫getTimeToLiveInSeconds和getTimeToLiveInMillis,會檢查是否超時,如果超時設定事務回滾,並丟擲TransactionTimedOutException異常。到此我們只要找到呼叫它們的位置就好了,那什麼地方呼叫的它們呢? 最簡單的辦法使用如“IntelliJ IDEA”中的“Find Usages”找到get***的使用地方;會發現:

DataSourceUtils.applyTransactionTimeout會呼叫DataSourceUtils.applyTimeout,DataSourceUtils.applyTimeout程式碼如下:

Java程式碼  收藏程式碼
  1. publicstaticvoid applyTimeout(Statement stmt, DataSource dataSource, int timeout) throws SQLException {  
  2.     Assert.notNull(stmt, "No Statement specified");  
  3.     Assert.notNull(dataSource, "No DataSource specified");  
  4.     ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);  
  5.     if (holder != null && holder.hasTimeout()) {  
  6.         // Remaining transaction timeout overrides specified value.
  7.         stmt.setQueryTimeout(holder.getTimeToLiveInSeconds());  
  8.     }  
  9.     elseif (timeout > 0) {  
  10.         // No current transaction timeout -> apply specified value.
  11.         stmt.setQueryTimeout(timeout);  
  12.     }  
  13. }  

其中其在stmt.setQueryTimeout(holder.getTimeToLiveInSeconds());中會呼叫getTimeToLiveInSeconds,此時就會檢查事務是否超時;

然後在JdbcTemplate中,執行sql之前,會呼叫其applyStatementSettings:其會呼叫DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout());設定超時時間;具體可以看其原始碼;

到此我們知道了在JdbcTemplate拿到Statement之後,執行之前會設定其queryTimeout,具體意思參考Javadoc:

3、結論

寫道 Spring事務超時 = 事務開始時到最後一個Statement建立時時間 + 最後一個Statement的執行時超時時間(即其queryTimeout)。

4、因此

假設事務超時時間設定為2秒;假設sql執行時間為1秒;

如下呼叫是事務不超時的

Java程式碼  收藏程式碼
  1. publicvoid testTimeout() throws InterruptedException {  
  2.     System.out.println(System.currentTimeMillis());  
  3.     JdbcTemplate jdbcTemplate = new JdbcTemplate(ds);  
  4.     jdbcTemplate.execute(" update test set hobby = hobby || '1'");  
  5.     System.out.println(System.currentTimeMillis());  
  6.     Thread.sleep(3000L);  
  7. }  

而如下事務超時是起作用的;

Java程式碼  收藏程式碼
  1. publicvoid testTimeout() throws InterruptedException {  
  2.     Thread.sleep(3000L);  
  3.     System.out.println(System.currentTimeMillis());  
  4.     JdbcTemplate jdbcTemplate = new JdbcTemplate(ds);  
  5.     jdbcTemplate.execute(" update test set hobby = hobby || '1'");  
  6.     System.out.println(System.currentTimeMillis());  
  7. }  

因此,不要忽略應用中如遠端呼叫產生的事務時間和這個事務時間是否對您的事務產生影響。

另外:

1、事務超時不起作用,您要首先檢查您的事務起作用了沒:可以參考使用Aop工具類診斷常見問題

3、如果您用JDBC,但沒有用JdbcTemplate,直接使用DateSourceUtils進行事務控制時,要麼自己設定Statement的queryTimeout超時時間,要麼使用TransactionAwareDataSourceProxy,其在建立Statement時會自動設定其queryTimeout。

4、關於JDBC超時時間設定一篇不錯的翻譯:深入理解JDBC的超時設定