IsolationLevel

交易隔離層級是用來控制「當多個交易同時讀寫資料時,要不要讓他們互相看到對方的變更。要資料「越準確」,就得「越鎖」,要系統「越快」,就得「越鬆」,是在討論競爭條件(Race condition)的議題
💼 READ UNCOMMITTED
允許交易去「偷看別人還沒寫完的資料」,這樣雖然速度快,但有風險,因為你讀到的資料可能會「被改回去」或「消失」
- 不會加共用鎖 (Shared Lock),所以別人正在改資料時,你一樣能讀它。
- 不會被排他鎖 (Exclusive Lock) 阻擋,所以就算別人正在修改資料、還沒提交,你也能去讀它。
🟢 Shared Lock(共用鎖)
是 SQL Server 在「讀資料」時加上的一種鎖,它的作用是我正在看資料,別人在我看完之前不能改它
觸發時機:SELECT 查詢(讀資料),可以多個共存 Shared Lock 但會阻擋 Exclusive Lock(別人不能改)
用途:確保你讀的資料在讀取過程中不會被修改
1 | BEGIN TRAN; |
預設情況下,EF 查詢會有 Shared Lock。因為 SQL Server 的預設隔離層級就是 READ COMMITTED。而 READ COMMITTED 的行為 = 查資料時會加 Shared Lock(直到讀完那筆資料)
🔴 Exclusive Lock(排他鎖)
用在「改資料」。別人不能看、也不能改,觸發時機是 INSERT、UPDATE、DELETE,完全不能共存(別人連讀都不行),會阻擋任何 Shared 或 Exclusive Lock,用途是確保修改時不會被別人讀或改,避免資料不一致
1 | BEGIN TRAN; |
💼 READ COMMITTED(已提交讀取)—SQL Server 預設
我只能讀「已經被提交」的資料。每次讀資料時會加 Shared Lock,讀完該筆資料就釋放(不是等整個交易結束),不會讀到未提交資料(避免髒讀),但因為釋放太快,可能再次查同筆資料會變了(不可重複讀)。
你只能看「圖書館正式版的書」,別人還沒交稿的不能看。但別人可能過 1 秒就更新再交稿 → 你再看一次內容就變了。
1 | BEGIN TRAN; |
一般瀏覽商品清單
1 | SELECT * FROM Products WHERE Category = 'Laptop'; |
使用者只是想看有哪些筆電、有多少庫存。資料就算在查詢過程中稍微變動(例如庫存少一台),也沒關係。
適合用 READ COMMITTED 因為不需要強一致性;查完就釋放 Shared Lock;效能高,並行性好。
像在電商網站上看商品頁面時,有人剛好下單一台筆電 → 你的畫面數字可能延遲幾秒更新,但不影響你購物體驗。
💼 REPEATABLE READ(可重複讀取)
希望整個交易期間,再次查相同條件的資料,內容都一樣
每次讀的資料都加 Shared Lock;鎖會一直保留到交易結束;別人不能修改這些資料;但別人可以插入新的資料(造成幻讀 Phantom Read)。
你在圖書館查資料時,書被你暫時「借出」,別人不能改內容。但圖書館可以新增新書(你再查一次時看到新資料)
在「查詢的當下」會加上 Shared Lock,查完就釋放,所以只會暫時卡住別人。
1 | BEGIN TRAN; |
在「整個交易期間」都保留 Shared Lock,直到 COMMIT 或 ROLLBACK 才釋放,所以會卡住別人整段時間
🛒 顧客結帳下單(扣庫存)
1 | BEGIN TRANSACTION; |
這裡如果用 READ COMMITTED,查庫存的那一瞬間加 Shared Lock,查完立刻放,結果在你 UPDATE 之前,另一個交易可能也剛好扣庫存,就可能發生「超賣」情況(overselling)。
👉 為了避免這種情況,會選擇 REPEATABLE READ 或 SERIALIZABLE,在整個交易中鎖住那筆資料,其他人不能修改庫存,直到你這邊 COMMIT,保證你查到的庫存值,不會被別人改。
🛒 錢包轉帳或支付交易
1 | BEGIN TRANSACTION; |
這時候千萬不能讓別人同時修改 UserA 的餘額,否則就會有「重複扣款」、「餘額錯亂」的風險。必須使用 REPEATABLE READ 或 SERIALIZABLE,保證這整個交易期間,查過的餘額不會被改;整個交易視為「一個原子操作」。就像銀行轉帳時會鎖定帳戶,確保你在扣款和入帳中間不會被別的交易干擾
💼 SERIALIZABLE(可序列化)
我要最嚴格的一致性。我讀到的資料範圍,別人不能改,也不能插入新的在這範圍內
REPEATABLE READ 會為你查過的每一筆資料加上 Shared Lock,且這些鎖會一直保留到交易結束
❌ 但不會鎖住整個查詢範圍 → 所以別人還是可以插入新的資料讓你下一次查多出結果(幻讀)。
鎖整個查詢範圍 (Range Lock);直到交易結束才釋放;沒有髒讀、沒有不可重複讀、沒有幻讀;但效能最低、最容易造成 blocking,因為他除了保留 Shared Lock 外,還會加上 Range Lock(範圍鎖)
🔹 檢查優惠券是否重複編號
1 | BEGIN TRAN; |
如果用 REPEATABLE READ,查詢沒鎖範圍,另一個交易在你插入前也查一次 → 看不到你的那筆;
結果兩人同時 INSERT 成功 → ❌ 重複代碼。
如果用 SERIALIZABLE,鎖住「Code = ‘ABC123’」這個索引範圍,第二個交易在你 COMMIT 前會被卡住;
✅ 保證不會有重複編號。
適合用於「要保證查詢條件的範圍不變」的場景,例如
- 檢查唯一條件(會員代碼、優惠券代碼、帳號註冊重複性)
- 統計或產生編號時避免重複
- 批次處理需要確保範圍不變(例如 select + insert)
💼 Lost Update(遺失更新)
Lost Update(遺失更新) 指的是兩個交易同時去修改同一筆資料,最後一個 Commit 的人,把前一個人的修改結果覆蓋掉,導致第一個人的更新「被吃掉」,沒有被保留!
想像 Ben 的帳戶餘額是 200 元。此時同時發生兩件事
- A:要扣 100 元(買東西)
- B:也要扣 100 元(轉帳)
A 的更新結果被 B 覆蓋,最終只保留了最後一個 Commit 的資料,這就是「遺失更新」
🔹 用「鎖」防止同時修改
設定隔離層級讓「讀取資料的人鎖住它」:方法 A:使用 REPEATABLE READ 或 SERIALIZABLE
讓當前交易查過的那筆資料,在交易結束前不能被別人修改
- A 查了 Ben 的資料 → Shared Lock 掛上
- B 想改 Ben 的資料 → 🚫 被卡住,要等 A COMMIT 才能動
→ 所以兩人不可能同時改,Lost Update 就不會發生。
「庫存扣減」或「帳戶扣款」都常這樣做
🔹 用「悲觀鎖 (Pessimistic Lock)」手動鎖資料
在程式層明確說:「我這筆要先鎖起來,別人別動。」
我要查這筆資料,順便先拿更新鎖(UPDLOCK),其他交易想改這筆要等我做完
1 | using var tran = await context.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted); |
這樣可以確保同時只有一個交易能改這筆資料
🔹 用「樂觀鎖 (Optimistic Concurrency)」
這是 EF Core 推薦 的方式,它不鎖資料,而是透過「版本號 (RowVersion / Timestamp)」檢查資料有沒有被別人改過
在資料表加一個 RowVersion 欄位:
1 | ALTER TABLE Accounts ADD RowVersion rowversion; |
在 EF 模型裡
1 | public class Account |
當兩個交易同時修改時
- A 先查 → RowVersion = 0x0001
- B 也查 → RowVersion = 0x0001
- A 存檔 → 成功,RowVersion 更新為 0x0002
- B 存檔 → EF 比對時發現 RowVersion 不同 → 丟出 DbUpdateConcurrencyException
你就能攔下這種情況,提示使用者「資料已被修改,請重新嘗試」。這就是「樂觀並行控制 (Optimistic Concurrency Control)」的典型作法
💼 Phantom
在同一個交易中,你用「同樣的查詢條件」查兩次,第二次結果多了或少了幾筆資料(通常是別人插入或刪除的),
感覺像資料「憑空多了一筆」或「少了一筆」——像幻覺一樣
🔹 使用 SERIALIZABLE 隔離層級
1 | SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; |
SQL Server 在這裡會使用 Range Lock(範圍鎖),把「Status = ‘Pending’」這個索引範圍整個鎖住。
其他交易若想新增符合這條件的資料,會被卡住直到你 COMMIT。效能最差(鎖的範圍大,並行性低)
🔹 使用 SNAPSHOT 隔離層級
1 | ALTER DATABASE MyDB SET ALLOW_SNAPSHOT_ISOLATION ON; |
我不鎖範圍,而是讀我當下開始交易那一刻的「資料快照」
✅ 不會被幻讀影響
✅ 不會鎖別人
⚙️ 效能高、並行性好
⚠️ 但需要資料庫開啟 ALLOW_SNAPSHOT_ISOLATION
| 情境 | 現象 | 影響 | 適用解法 |
|---|---|---|---|
| 查詢「待出貨」訂單清單 | 同時有人新增了新訂單 | 查第二次多一筆(幻讀) | SERIALIZABLE / SNAPSHOT |
| 查詢「庫存 < 10」商品 | 同時有商品新補貨或新上架 | 清單筆數變 | SERIALIZABLE / SNAPSHOT |
| 查詢「今日下單數量」 | 有人剛下單 | 統計數字變 | SNAPSHOT(用快照確保一致) |
💼 Write Skew
兩個交易同時「讀取相同條件下不同資料」,都判斷「目前狀況合法」,於是各自修改不同資料,結果整體狀態卻不再符合業務規則。
假設有一個規則:每個時段至少要有兩位醫生 on call
🧍♂️ Transaction A(Leo 請假)
1️⃣ 查目前 on_call 的醫生數量 → 2
2️⃣ 覺得符合規範(>= 2)→ 可以請假
3️⃣ UPDATE Leo 的狀態為 off_call
🧍♀️ Transaction B(Mark 請假)
1️⃣ 同時間查 on_call 的醫生數量 → 2
2️⃣ 覺得符合規範(>= 2)→ 也可以請假
3️⃣ UPDATE Mark 的狀態為 off_call
🔹 使用 SERIALIZABLE 隔離層級
這是最直接(也是最保守)的做法。在 SERIALIZABLE 模式下,SQL Server 會對查詢條件加「範圍鎖 (Range Lock)」
🔹 用「明確鎖」(Pessimistic Lock)
如果你不想用整個 SERIALIZABLE 隔離層級,也可以手動鎖定資料。
1 | SELECT * FROM Doctors WITH (XLOCK, HOLDLOCK) WHERE OnCall = 1; |
- XLOCK:強制加排他鎖
- HOLDLOCK:等效於 SERIALIZABLE 的鎖持續時間
這樣就確保在交易結束前,其他人不能同時查詢或修改這一批資料,本質上就是「悲觀鎖」策略
🔹 加入「業務層檢查」與「唯一條件」
在應用程式層,也可以加上「防守性邏輯」
1 | BEGIN TRAN; |
在同一交易裡「查 + 判斷 + 更新」一次完成。即使有其他人同時請假,也會被鎖機制擋住


