系統遭遇亂流,有段 ADO.NET 程式開啟 Transaction 進行資料庫更新,執行期間會呼叫第三方 Web Service,依回傳結果決定 Commit 或 Rollback。這幾天因 Web Service 異常,偶爾執行時間會超過 60 秒(正常狀況應不超過 3-5 秒),超出預設的 WCF 呼叫時間上限(SendTimeout),出錯後進入 Rollback 作業卻噴出以下錯誤:

The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION. 
The transaction active in the session has been commited or aborted by another session.
ROLLBACK TRANSACTION 要求沒有對應的 BEGIN TRANSACTION。
此工作階段的使用中交易,已由其他工作階段認可或中止。

清查相關程式碼,找不到出其他 Commit 或 Rollback 邏輯,交易被誰中斷是個謎。嘗試忽略錯誤改為直接 Commit 一樣會失敗,但冒出的錯誤訊息不同:

This SqlTransaction has completed; it is no longer usable

重新檢視程式碼發現可疑處 - 這段 SqlTransaction 動作被包在 TransactionScope 中,會不會是 TransactionScope 搞的鬼?

爬文證實了這點:

The TransactionScope class uses a default timeout (TransactionManager.DefaultTimeout, which has a default value of 1 minute). When the transaction timed out, it attempted to issue an abort command.
出處:TransactionScope Has a Default Timeout

TransactionScope 的預設時限為一分鐘,一旦逾時將試圖中止交易。依據此推論,用以下程式重現問題:

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Transactions" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<script runat="server">
string cs = "Data Source=...";
void Page_Load(object sender, EventArgs e)
{
    using (var tx = new TransactionScope()) 
    {
        using (var cn = new SqlConnection(cs)) 
        {
            cn.Open();
            var trn = cn.BeginTransaction();
            var cmd = cn.CreateCommand();
            cmd.Transaction = trn;
            cmd.CommandText = "SELECT Name FROM MyTable WHERE SN = 1";
            var dr = cmd.ExecuteReader();
            dr.Read();
            Response.Write("<li>" + dr[0]);
            dr.Close();
            cmd.CommandText = 
                "UPDATE MyTable SET Name = 'Jeffrey" + DateTime.Now.ToString("fff") + "' WHERE SN = 1";
            cmd.ExecuteNonQuery();
            //實際案例為呼叫 WebService 依結果決定 Commit 或 Rollback,此處以 Thread.Sleep 模擬
            System.Threading.Thread.Sleep(61000);
            trn.Commit();
            cmd.CommandText = "SELECT Name FROM MyTable WHERE SN = 1";
            dr = cmd.ExecuteReader();
            dr.Read();
            Response.Write("<li>" + dr[0]);
            tx.Complete();
        }
    }
}
</script>

在 TransactionScope 範圍內,我用 SqlConnection.BeginTransaction() 啟動交易,執行 UPDATE 更新後故意以 Thread.Sleep(60000) 等待 61 秒再執行 SqlTransaction.Commit(),藉此成功觸發 This SqlTransaction has completed; it is no longer usable 錯誤:

同樣的程式,縮短 Sleep() 時間到 60 秒以下或將 TransactionScope 移除,都可成功 Commit,證明問題源自 TransactionScope 預設的一分鐘上限。要調整 TransactionScope 時限,可在建構式傳入 scopeTimeout 參數 - new TransactionScope(TransactionScopeOption.Required, new TimeSpan(0, 2, 0)) 或透過 web.config 修改預設值:

 <system.transactions>
    <defaultSettings timeout="00:02:00" />
  </system.transactions>

如此即可將 TransactionScope 時限延長,但要提醒 - Transaction 期間拖得太久,鎖定期間拉長將阻礙資料存取損害效能,實屬下策。根本之道應是力求速戰速決,讓 Transaction 從開始到結束的期間愈短愈好,才是打造優質系統的正確姿勢。

A case of how TransactionScope default timeout affects normal transaction operation.


Comments

# by 大師兄

把transactionscope裡面的transaction拿掉不是就可以了嗎?為什麼還要去改什麼timeout時間

# by Jeffrey

to 大師兄, 1) 如超過時限,呼叫 TransactionScope.Complete() 動作一樣會失敗 2) 實際上 SqlTransaction 動作有可能寫在第三方程式庫裡,加 TransactionScope 的目的是整合多個資料庫的交易,程式庫的行為非我們所能修改

# by 2

1

# by 3

2

Post a comment