手邊的專案遇到一個情境: TransactionScope中包含兩段SQL操作,因未共用連線,預期將啟動MSDTC分散式交易。而第二段SQL操作使用了T-SQL的BEGIN TRAN與COMMIT TRAN。若TrasactionScope未能呼叫TransactionScope.Complete(),第二段COMMIT TRAN的DB寫入動作是否會Rollback? 這個議題,引起討論。

依我的理解,BEGIN TRAN/COMMIT TRAN形成的子交易,仍會參與當下的環境交易(Ambient Transaction,包在using TransactionScope範圍的資料庫操作,預設會參與該TransactionScope所建立的交易,該交易即所謂環境交易[參考]),當環境交易Rollback(亦即TransactionScope未呼叫Complete()),子交易將會跟著Rollback。

由於信仰不夠堅定,無法100%確認自己的認知無誤,故設計實驗進行驗證:

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Transactions;
 
namespace TestTSqlCommit
{
    class Program
    {
        static void truncateTable()
        {
            using (SqlConnection cn = new SqlConnection(getDiffCnStr()))
            {
                cn.Open();
                var cmd = cn.CreateCommand();
                cmd.CommandText = "TRUNCATE TABLE LockLab";
                cmd.ExecuteNonQuery();
            }
        }
 
 
        static string cs = "Data Source=...省略...;Application Name={0}";
        //使用隨機Application Name建立連線字串
        static string getDiffCnStr()
        {
            return string.Format(cs, Guid.NewGuid());
        }
 
        static void Main(string[] args)
        {
            //將LockLab資料表清空
            truncateTable();
            //建立TransactionScope
            using (TransactionScope tx = new TransactionScope())
            {
                try
                {
                    //以隨機式連線字串建立連線,確保觸發分散式交易
                    //參考: http://blog.darkthread.net/post-2010-11-12-msdtc-2008.aspx 
                    using (SqlConnection cn = new SqlConnection(getDiffCnStr()))
                    {
                        cn.Open();
                        var cmd = cn.CreateCommand();
                        cmd.CommandText = 
"INSERT INTO LockLab VALUES ('TEST','201302', 1)";
                        cmd.ExecuteNonQuery();
                    }
                    //第二段SQL 使用COMMIT TRAN
                    using (SqlConnection cn = new SqlConnection(getDiffCnStr()))
                    {
                        cn.Open();
                        var cmd = cn.CreateCommand();
                        cmd.CommandText = @"
BEGIN TRAN
INSERT INTO LockLab VALUES ('JEFF','201302', 1)
COMMIT TRAN
";
                        cmd.ExecuteNonQuery();
                    }
 
                    throw new ApplicationException("刻意觸發錯誤!");
                    //tx.Complete()不會被執行,Transaction應Rollback
                    tx.Complete();
                }
                catch (Exception ex)
                {
                    Console.Write(ex.ToString());
                }
            }
            Console.Read();
        }
    }
}

測試結果如同預期,TEST與JEFF兩筆資料均未寫入,證實TransactionScope中即便BEGIN TRAN/COMMIT TRAN仍會參與交易,可被Rollback。

最後補充,如果我們希望子交易不要依附於外層TransactionScope,以實現不論外層Transaction Rollback與否,子交易需能自由控制是否Commit的結果。為此,我們可宣告一個TransactionScopeOption.RequiresNew模式的TransactionScope將子交易包起來,則其中將自成一個獨立交易範圍,不受環境交易影響。沿用上例,如要將第二段獨立自成交易,可修改為:

                    using (TransactionScope tx2 = 
                        new TransactionScope(TransactionScopeOption.RequiresNew))
                    {
                        using (SqlConnection cn = new SqlConnection(getDiffCnStr()))
                        {
                            cn.Open();
                            var cmd = cn.CreateCommand();
                            cmd.CommandText = @"
BEGIN TRAN
INSERT INTO LockLab VALUES ('JEFF','201302', 1)
COMMIT TRAN
";
                            cmd.ExecuteNonQuery();
                        }
                        tx2.Complete();
                    }

如此,即便tx.Complete()未執行,JEFF資料也會寫入DB。


Comments

# by Ivan

那麼如果子交易為TransactionScopeOption.RequiresNew, 當子交易rollback , 可會令母交易rollback呢?

# by Tom Tang

@Ivan 當使用 RequiresNew, 已經沒有所謂『母子』的關係。 兩個 scope 毫無相關,不論外層或內層誰 complete / rollback,都不影響另一者。

# by Ho.Chun

有點不太懂,所以 TransactionScope 可以用來處理跨 DB 的 Transaction 嗎 ?

Post a comment