☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
資料庫 進階

交易與並行控制

什麼是交易(Transaction)?

想像你在銀行轉帳:

  1. 從 A 帳戶扣 1000 元
  2. 在 B 帳戶加 1000 元

這兩步必須同時成功或同時失敗。如果扣了 A 的錢但 B 沒加到,錢就憑空消失了! 這就是交易(Transaction)要解決的問題。


ACID 特性

特性 英文 說明 銀行比喻
原子性 Atomicity 全部成功或全部失敗 轉帳要嘛成功,要嘛完全沒動
一致性 Consistency 交易前後資料都合法 總金額不會改變
隔離性 Isolation 交易之間互不干擾 你轉帳時別人看不到中間狀態
持久性 Durability 完成後永久保存 轉完帳重開機錢不會消失

EF Core 中的交易

隱式交易(預設)

// SaveChanges 自動包在交易裡
var student = new Student { Name = "小賢" }; // 建立學生
db.Students.Add(student);                      // 加入追蹤

var course = new Course { CourseName = "C#" }; // 建立課程
db.Courses.Add(course);                          // 加入追蹤

await db.SaveChangesAsync();                     // 一次送出 → 自動包在交易裡
// 如果任何一個 INSERT 失敗,全部都會回退(Rollback)

明確交易

// 需要跨多次 SaveChanges 的交易
using var transaction = await db.Database    // 開始一個交易
    .BeginTransactionAsync();                // 取得交易物件

try
{
    // 步驟 1:從 A 帳戶扣款
    var accountA = await db.Accounts         // 查詢 A 帳戶
        .FirstAsync(a => a.Id == 1);         // 取得帳戶資料
    accountA.Balance -= 1000;                // 扣 1000 元
    await db.SaveChangesAsync();             // 儲存(但交易尚未提交)

    // 步驟 2:在 B 帳戶加款
    var accountB = await db.Accounts         // 查詢 B 帳戶
        .FirstAsync(a => a.Id == 2);         // 取得帳戶資料
    accountB.Balance += 1000;                // 加 1000 元
    await db.SaveChangesAsync();             // 儲存(但交易尚未提交)

    // 兩步都成功,提交交易
    await transaction.CommitAsync();         // 提交交易(真正寫入資料庫)
}
catch (Exception ex)                         // 任何步驟出錯
{
    await transaction.RollbackAsync();       // 回退所有變更
    Console.WriteLine($"交易失敗: {ex.Message}"); // 記錄錯誤
    throw;                                   // 重新拋出例外
}

Dapper 中的交易

using var conn = new SqlConnection(connectionString); // 建立連線
await conn.OpenAsync();                                // 開啟連線

using var transaction = conn.BeginTransaction();       // 開始交易

try
{
    // 步驟 1:扣款
    await conn.ExecuteAsync(                           // 執行 SQL
        "UPDATE Accounts SET Balance = Balance - @Amount WHERE Id = @Id", // 扣款語句
        new { Amount = 1000, Id = 1 },                 // 參數
        transaction                                    // 傳入交易物件
    );

    // 步驟 2:加款
    await conn.ExecuteAsync(                           // 執行 SQL
        "UPDATE Accounts SET Balance = Balance + @Amount WHERE Id = @Id", // 加款語句
        new { Amount = 1000, Id = 2 },                 // 參數
        transaction                                    // 傳入同一個交易物件
    );

    transaction.Commit();                              // 提交交易
}
catch
{
    transaction.Rollback();                            // 回退交易
    throw;                                             // 重新拋出
}

隔離等級(Isolation Level)

不同的隔離等級決定了交易之間的可見性

隔離等級 髒讀 不可重複讀 幻讀 效能
Read Uncommitted ✅ 可能 ✅ 可能 ✅ 可能 最快
Read Committed(預設) ❌ 防止 ✅ 可能 ✅ 可能
Repeatable Read ❌ 防止 ❌ 防止 ✅ 可能 中等
Serializable ❌ 防止 ❌ 防止 ❌ 防止 最慢

名詞解釋

  • 髒讀:讀到別人還沒提交的資料(可能被回退)
  • 不可重複讀:同一交易中,兩次讀取同一列得到不同結果
  • 幻讀:同一交易中,兩次查詢得到不同數量的列
// 在 EF Core 中設定隔離等級
using var transaction = await db.Database.BeginTransactionAsync(
    System.Data.IsolationLevel.Serializable // 最嚴格的隔離等級
);
// ⚠️ Serializable 最安全但最慢,只在真正需要時使用
-- 在 SQL 中設定隔離等級
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 設定隔離等級
BEGIN TRANSACTION;                               -- 開始交易
    SELECT * FROM Accounts WHERE Id = 1;         -- 查詢帳戶
    UPDATE Accounts SET Balance = Balance - 1000 WHERE Id = 1; -- 扣款
COMMIT TRANSACTION;                              -- 提交交易

樂觀並行控制 vs 悲觀並行控制

樂觀並行控制(Optimistic Concurrency)

假設衝突很少發生,先做再說,提交時再檢查是否有衝突。

// 在 Entity 中加入 RowVersion(並行權杖)
public class Product                           // 產品 Entity
{
    public int Id { get; set; }                // 主鍵
    public string Name { get; set; } = "";   // 產品名稱
    public decimal Price { get; set; }         // 價格
    public int Stock { get; set; }             // 庫存

    [Timestamp]                                // 標記為並行權杖
    public byte[] RowVersion { get; set; } = null!; // 每次更新自動變更
}

// Fluent API 設定
modelBuilder.Entity<Product>()                 // 設定 Product Entity
    .Property(p => p.RowVersion)               // RowVersion 屬性
    .IsRowVersion();                           // 標記為列版本

// 使用時自動檢查並行衝突
try
{
    var product = await db.Products            // 查詢產品
        .FirstAsync(p => p.Id == 1);           // 取得 ID=1 的產品
    product.Stock -= 1;                        // 減少庫存
    await db.SaveChangesAsync();               // 儲存時會檢查 RowVersion
}
catch (DbUpdateConcurrencyException ex)        // 並行衝突例外
{
    // 有人在你之前修改了這筆資料!
    var entry = ex.Entries.Single();           // 取得衝突的 Entity
    var dbValues = await entry.GetDatabaseValuesAsync(); // 取得資料庫最新值
    // 決定要:1. 用資料庫的值 2. 用你的值 3. 合併
    entry.OriginalValues.SetValues(dbValues!); // 用資料庫最新值重試
    await db.SaveChangesAsync();               // 重新儲存
}

悲觀並行控制(Pessimistic Concurrency)

假設衝突經常發生,先鎖定資源再操作。

-- 在 SQL 中使用悲觀鎖定
BEGIN TRANSACTION;                             -- 開始交易

SELECT * FROM Products WITH (UPDLOCK, ROWLOCK) -- 鎖定這一列
WHERE Id = 1;                                  -- 其他交易無法修改這列

UPDATE Products SET Stock = Stock - 1          -- 更新庫存
WHERE Id = 1;                                  -- 更新指定產品

COMMIT TRANSACTION;                            -- 提交並釋放鎖定
// 在 EF Core 中使用原始 SQL 實現悲觀鎖定
using var transaction = await db.Database      // 開始交易
    .BeginTransactionAsync();

var product = await db.Products                // 使用原始 SQL 查詢並鎖定
    .FromSqlRaw("SELECT * FROM Products WITH (UPDLOCK) WHERE Id = {0}", 1) // 加鎖
    .FirstAsync();                             // 取得產品

product.Stock -= 1;                            // 減少庫存
await db.SaveChangesAsync();                   // 儲存變更
await transaction.CommitAsync();               // 提交交易並釋放鎖

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:長時間持有交易

// ❌ 在交易中做耗時操作
using var transaction = await db.Database.BeginTransactionAsync(); // 開始交易
var data = await db.Products.ToListAsync();   // 查詢(鎖定資源)

await SendEmailAsync(data);                   // ❌ 發送 Email(可能要好幾秒!)
await CallExternalApiAsync();                 // ❌ 呼叫外部 API(更慢!)

await db.SaveChangesAsync();                  // 儲存
await transaction.CommitAsync();              // 提交
// 在整段期間,其他交易都被擋住了!可能導致 Timeout
// ✅ 交易中只做資料庫操作,其他的放在交易外面
var data = await db.Products.ToListAsync();   // 先查詢資料(交易外)

using var transaction = await db.Database.BeginTransactionAsync(); // 開始交易
// 只做必要的資料庫操作
product.Stock -= 1;                           // 更新資料
await db.SaveChangesAsync();                  // 儲存
await transaction.CommitAsync();              // 提交(快速完成)

// 交易結束後再做其他操作
await SendEmailAsync(data);                   // 發送 Email(交易外)
await CallExternalApiAsync();                 // 呼叫外部 API(交易外)

❌ 錯誤 2:死鎖(Deadlock)

// ❌ 兩個交易互相等待對方釋放鎖定
// 交易 A:先鎖 Products → 再鎖 Orders
// 交易 B:先鎖 Orders → 再鎖 Products
// 結果:A 等 B 釋放 Orders,B 等 A 釋放 Products → 死鎖!
// ✅ 所有交易都按照相同的順序存取資源
// 交易 A 和 B 都先鎖 Orders → 再鎖 Products
// 這樣就不會產生循環等待

❌ 錯誤 3:忘記 Rollback

// ❌ 沒有 try-catch,異常時交易不會回退
var transaction = await db.Database.BeginTransactionAsync(); // 開始交易
await db.SaveChangesAsync();                  // 如果這裡拋出例外...
await transaction.CommitAsync();              // 這行不會執行
// 交易會一直掛著,佔用資源,直到連線超時
// ✅ 用 using + try-catch 確保交易正確處理
using var transaction = await db.Database.BeginTransactionAsync(); // using 確保釋放
try
{
    await db.SaveChangesAsync();              // 儲存變更
    await transaction.CommitAsync();          // 提交交易
}
catch
{
    await transaction.RollbackAsync();        // 明確回退
    throw;                                    // 重新拋出例外
}

💡 重點整理

概念 說明
ACID 原子性、一致性、隔離性、持久性
SaveChanges EF Core 自動包在隱式交易中
BeginTransaction 明確開始一個交易
Isolation Level 控制交易之間的可見性
樂觀並行 用 RowVersion 檢查衝突
悲觀並行 用 UPDLOCK 鎖定資源
死鎖 兩個交易互相等待 → 統一存取順序避免

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊