事務超時時間的錯誤理解

java開發,無非數據庫,spring等一些技術,在公司碼代碼,一直有用到事務這個東西,按說對這個也很熟悉了,今天突然發現一個"奇怪"的現象.

首先pom文件是這樣的,用的spring-boot1.5.20,spring版本爲<spring.version>4.3.23.RELEASE</spring.version>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.20.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>

寫了如下的代碼,設置事務超時時間爲2s,線程休眠了3s.

首先下面這樣的寫法事務是肯定會起作用的

import org.springframework.transaction.annotation.Transactional;

    @Autowired
    private TestRepository testRepository;

    @Override
    @Transactional(propagation =Propagation.REQUIRES,rollbackFor = Exception.class, timeout = 2)
    public void test() throws Exception {
        Thread.sleep(3000);
        Test a = new Test();
        a.setMsg("TEST");
        a.setFlag(true);
        Test save = testRepository.save(a);
        System.out.println(save);
        //Thread.sleep(3000);
    }

重點來了

如果我現在使用這種寫法的話,事務就不管用了~
import org.springframework.transaction.annotation.Transactional;

    @Autowired
    private TestRepository testRepository;

    @Override
    @Transactional(propagation =Propagation.REQUIRES,rollbackFor = Exception.class, timeout = 2)
    public void test() throws Exception {
        //Thread.sleep(3000);
        Test a = new Test();
        a.setMsg("TEST");
        a.setFlag(true);
        Test save = testRepository.save(a);
        System.out.println(save);
        Thread.sleep(3000);
    }

出現這種的問題,肯定不是靈異事件,這肯定是自己對事務的認識肯定有問題的,所以我查了很多資料,但是仍然沒有看明白,最後終於找到了.

在此需要分析下DataSourceTransactionManager的源碼,我發現這裏會先調用doBegin這個方法

--------
	int timeout = determineTimeout(definition);
	if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
		txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
	}
-------

這個時候就會發現TransactionDefinition.TIMEOUT_DEFAULT,這個是我們在爲事務設置超時時間的參數,我們繼續往下找

	/**
	 * Set the timeout for this object in seconds.
	 * @param seconds number of seconds until expiration
	 */
	public void setTimeoutInSeconds(int seconds) {
		setTimeoutInMillis(seconds * 1000L);
	}

	/**
	 * Set the timeout for this object in milliseconds.
	 * @param millis number of milliseconds until expiration
	 */
	public void setTimeoutInMillis(long millis) {
		this.deadline = new Date(System.currentTimeMillis() + millis);
	}

這時候找了dealine這個參數,在ResourceHolderSupport這個類中,我發現了這幾個方法使用了它.


	/**
	 * Return the time to live for this object in seconds.
	 * Rounds up eagerly, e.g. 9.00001 still to 10.
	 * @return number of seconds until expiration
	 * @throws TransactionTimedOutException if the deadline has already been reached
	 */
	public int getTimeToLiveInSeconds() {
		double diff = ((double) getTimeToLiveInMillis()) / 1000;
		int secs = (int) Math.ceil(diff);
		checkTransactionTimeout(secs <= 0);
		return secs;
	}

	/**
	 * Return the time to live for this object in milliseconds.
	 * @return number of millseconds until expiration
	 * @throws TransactionTimedOutException if the deadline has already been reached
	 */
	public long getTimeToLiveInMillis() throws TransactionTimedOutException{
		if (this.deadline == null) {
			throw new IllegalStateException("No timeout specified for this resource holder");
		}
		long timeToLive = this.deadline.getTime() - System.currentTimeMillis();
		checkTransactionTimeout(timeToLive <= 0);
		return timeToLive;
	}

	/**
	 * Set the transaction rollback-only if the deadline has been reached,
	 * and throw a TransactionTimedOutException.
	 */
	private void checkTransactionTimeout(boolean deadlineReached) throws TransactionTimedOutException {
		if (deadlineReached) {
			setRollbackOnly();
			throw new TransactionTimedOutException("Transaction timed out: deadline was " + this.deadline);
		}
	}

實際上getTimeToLiveInSecondsgetTimeToLiveInMillis都調用了checkTransactionTimeout這個方法,這個checkTransactionTimeout方法中將rollbackOnly設置爲了true,然後拋出TransactionTimedOutException異常.我們繼續看getTimeToLiveInSeconds被誰調用.

DataSourceUtils中的applyTimeout方法中,

	/**
	 * Apply the specified timeout - overridden by the current transaction timeout,
	 * if any - to the given JDBC Statement object.
	 * @param stmt the JDBC Statement object
	 * @param dataSource the DataSource that the Connection was obtained from
	 * @param timeout the timeout to apply (or 0 for no timeout outside of a transaction)
	 * @throws SQLException if thrown by JDBC methods
	 * @see java.sql.Statement#setQueryTimeout
	 */
	public static void applyTimeout(Statement stmt, DataSource dataSource, int timeout) throws SQLException {
		Assert.notNull(stmt, "No Statement specified");
		ConnectionHolder holder = null;
		if (dataSource != null) {
			holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
		}
		if (holder != null && holder.hasTimeout()) {
			// Remaining transaction timeout overrides specified value.
			stmt.setQueryTimeout(holder.getTimeToLiveInSeconds());
		}
		else if (timeout >= 0) {
			// No current transaction timeout -> apply specified value.
			stmt.setQueryTimeout(timeout);
		}
	}

在這裏,繼續查看方法調用情況applyTimeoutJdbcTemplate中的applyStatementSettings方法調用,如下圖所示:
applyStatementSettings方法調用情況在執行sql語句時,無論使用的是jpa還是jdbctemplate,最後都會進入jdbctemplate中的execute方法

	@Override
	public <T> T execute(StatementCallback<T> action) throws DataAccessException {
		Assert.notNull(action, "Callback object must not be null");

		Connection con = DataSourceUtils.getConnection(getDataSource());
		Statement stmt = null;
		try {
			Connection conToUse = con;
			if (this.nativeJdbcExtractor != null &&
					this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
				conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
			}
			stmt = conToUse.createStatement();
			applyStatementSettings(stmt);
			Statement stmtToUse = stmt;
			if (this.nativeJdbcExtractor != null) {
				stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
			}
			T result = action.doInStatement(stmtToUse);
			handleWarnings(stmt);
			return result;
		}
		catch (SQLException ex) {
			// Release Connection early, to avoid potential connection pool deadlock
			// in the case when the exception translator hasn't been initialized yet.
			JdbcUtils.closeStatement(stmt);
			stmt = null;
			DataSourceUtils.releaseConnection(con, getDataSource());
			con = null;
			throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
		}
		finally {
			JdbcUtils.closeStatement(stmt);
			DataSourceUtils.releaseConnection(con, getDataSource());
		}
	}

實際上到這裏就已經很明朗了,實際上在執行sql語句前,先執行了判斷是否含有事務超時時間,所以當方法體重如果有超時的情況,就會直接關閉這個jdbc的連接.不會去連接數據庫.

但是已經執行完數據庫操作後,再進行其他操作,並且沒有其他數據庫操作的話,這個超時時間是不會起作用的,因爲根本就不會執行數據庫的操作,所以這個當然不會回滾了.

從中可以發現,還是對於spring的理解不夠深刻,或者說只是自己的愚昧,-_-||,還是需要多看源碼喲~還是應該多學習!

以上都是在網上博客上找到的原因,自己又進行了查驗,從中也學到了不少,感謝原博主!

另外:解決方法其實也有很多種:這裏總結幾個處理的方法

  • 使用手動回滾的方法:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  • 直接顯式拋出異常throw new RuntimeException();

參考博客:

1.Spring事務超時時間可能存在的錯誤認識2- https://blog.csdn.net/educast/article/details/78823970
2.Spring事務超時時間可能存在的錯誤認識2- https://www.iteye.com/blog/m635674608-2302007

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章