TransactionScope與COMMIT TRAN

3

「如果用 TransactionScope 包了一段程式,裡面又在 SQL 裡手動開 BEGIN TRAN,那最後誰決定要不要真的 commit?」

4

只要你在 using 範圍內操作資料庫,EF 或 ADO.NET 會自動參與這個「環境交易 (Ambient Transaction)」。如果沒呼叫 scope.Complete(),在離開 using 時就會自動 rollback

1
2
3
4
5
6
7
8
9
using (var scope = new TransactionScope())
{
// 這裡做的所有 DB 操作(只要使用相同的連線)都會自動參與這個交易
DoSql1();
DoSql2();

// 決定是否 commit
scope.Complete();
}

3

這是 T-SQL 層級的交易,寫在 SQL 裡面

1
2
3
BEGIN TRAN
INSERT INTO Orders ...
COMMIT TRAN

這是 資料庫端的交易。它可以存在於

  • 一個獨立的連線(沒有 Ambient Transaction
  • 某個由 .NET 傳進來的環境交易裡面

當你在 C# 中用 TransactionScope 包起來後,你建立的每個資料庫連線,預設會自動加入這個環境交易 (Ambient Transaction),所以 BEGIN TRAN 這個「SQL 裡的交易」其實只是「子交易」或「巢狀交易」,它會參與外部的環境交易 (Ambient Transaction)

1
2
3
TransactionScope (老大)
├── Connection 1:SQL 操作
└── Connection 2:SQL 操作(裡面有 BEGIN/COMMIT TRAN)

當 TransactionScope 最後沒有 .Complete(),整個 Ambient Transaction 會被 Rollback,即使第二段 SQL 在資料庫層看起來「COMMIT 成功」,實際上那只是該連線內的子交易結束,最終整個分散式交易 (MSDTC) 會通知資料庫 rollback

1
2
3
4
5
static string cs = "Data Source=...省略...;Application Name={0}";
static string getDiffCnStr()
{
return string.Format(cs, Guid.NewGuid());
}

每次建立連線時,都使用不同的 Application Name。因為 SQL Server 的連線池 (Connection Pool) 會根據連線字串內容共用連線,這樣做可以強迫建立不同的連線,確保之後會觸發 MSDTC 分散式交易

3

清空測試用的 LockLab 表

1
2
// 將LockLab資料表清空
truncateTable();

g

建立TransactionScope

1
2
3
4
using (TransactionScope tx = new TransactionScope())
{
}

f

以隨機式連線字串建立連線,確保觸發分散式交易

1
using (SqlConnection cn = new SqlConnection(getDiffCnStr()))

第一段 sql, Insert

1
2
3
4
5
6
7
8
9
10
//以隨機式連線字串建立連線,確保觸發分散式交易
//參考: 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();
}

3

第二段 SQL 使用COMMIT TRAN

1
2
3
4
5
6
7
8
9
10
11
12
//第二段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();
}

f

觸發 Rollback

1
2
3
throw new ApplicationException("刻意觸發錯誤!");
//tx.Complete()不會被執行,Transaction應Rollback
tx.Complete();

f

Overview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

// 將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();

static void truncateTable()
{
using (SqlConnection cn = new SqlConnection(getDiffCnStr()))
{
cn.Open();
var cmd = cn.CreateCommand();
cmd.CommandText = "TRUNCATE TABLE LockLab";
cmd.ExecuteNonQuery();
}
}
步驟 動作 狀態
1 第一段 SQL 插入 'TEST' 在交易中,未提交
2 第二段 SQL 插入 'JEFF'COMMIT TRAN 子交易 Commit,但仍屬於外層交易
3 丟出 Exception,未呼叫 tx.Complete() TransactionScope 結束時 Rollback 整個 MSDTC 交易
4 結果 LockLab 仍是空表(兩筆資料都被 Rollback)

即使第二段 SQL 在程式裡明確執行了 COMMIT TRAN,只要它是在 TransactionScope 的範圍內,就會被視為外層環境交易 (Ambient Transaction) 的子交易。當外層 TransactionScope 沒呼叫 .Complete(),整個分散式交易會 Rollback → ✅ 結果是所有 Insert 都不會留下。這就是為什麼這段程式執行完後 LockLab 仍是空的

3

3

ˇ

4