[後端] 資料庫交易與隔離等級白話講:為什麼你轉帳不能只做一半?
前言
工程師們,大家午安!今天想跟大家聊一個看似老生常談,但其實超級重要、而且常常被誤解的概念:資料庫交易(Transaction) 跟 隔離等級(Isolation Level)。
欸,不要看到「交易」兩個字就想說跟錢有關就頭痛,它跟我們每天寫的程式、處理的資料息息相關。想想看,你平常在開發的時候,有沒有遇過這樣的狀況:
- 客戶回報說,他們的訂單狀態明明已經成功,但庫存卻沒有扣到?
- 或者使用者抱怨,他剛買完票,但重新整理頁面後,票又可以買了?
- 更嚴重一點,你轉帳給朋友,結果你的錢扣了,朋友卻沒收到?
這些問題的背後,常常都跟「資料庫交易」沒搞清楚,或者「隔離等級」設定不對有關係。今天,我們就來用最白話的方式,把這些概念拆解開來,讓你知道為什麼不能只做一半,以及怎麼在效能跟資料一致性之間做取捨。
什麼是「交易 (Transaction)」?為什麼你轉帳不能只做一半?
想像一下你去銀行轉帳,整個流程通常是這樣:
- 從你的帳戶扣錢
- 把錢加到對方的帳戶
這兩個步驟必須「同時成功」或「同時失敗」,不能只完成其中一個。如果你的錢扣了,對方卻沒收到,那麻煩就大了。這個「要嘛全成功、要嘛全失敗」的原子操作集合,就是 **交易 (Transaction)**。
在資料庫領域,我們通常用 ACID 這四個字來描述一個交易應該具備的特性:
1. 原子性 (Atomicity):要嘛全成功,要嘛全失敗
這就是我前面說的「轉帳」例子。所有步驟綁在一起,像一個不可分割的原子。如果交易中的任何一個操作失敗,整個交易就會被取消(Rollback),所有變更都會回復到交易開始前的狀態。
1 | -- 開始一個交易 |
2. 一致性 (Consistency):交易前後資料必須維持合法狀態
資料庫從一個合法的狀態開始,經過一個交易後,會到達另一個合法的狀態。舉例來說,如果規定帳戶餘額不能是負數,那轉帳交易完成後,所有帳戶餘額都必須是非負數。這個特性通常是透過資料庫的約束(Constraint)、觸發器(Trigger)和程式碼邏輯來保證的。
3. 隔離性 (Isolation):交易之間互不影響
多個交易同時執行時,它們的執行結果應該與它們「依序執行」的結果相同。白話來說,就是一個交易在進行的時候,不應該看到其他還沒完成的交易對資料做了什麼修改。這點非常重要,也是我們今天文章的重點「隔離等級」在處理的問題。
4. 持久性 (Durability):交易提交後,資料永不消失
一旦交易成功提交(Commit),即使系統發生故障(例如斷電),資料庫中的變更也應該永久保存下來。這通常是透過將資料寫入永久儲存裝置(硬碟)來實現的。
為什麼需要「隔離等級」?多個人同時操作資料,問題就來了!
想像一下,一個資料庫同時有成千上萬個使用者在操作,都在讀取、寫入、更新資料。如果沒有適當的隔離機制,這些同時執行的交易就會互相干擾,導致資料錯誤。這就像多個人同時編輯一份文件,如果沒有鎖定機制,就會亂成一團。
資料庫系統為了處理這種併發(Concurrency)問題,提供了不同的 隔離等級(Isolation Level)。不同的隔離等級,就是用來定義一個交易在執行時,能夠「看到」多少來自其他併發交易的資料變更。隔離等級越高,資料的一致性越好,但通常效能會越差;反之,隔離等級越低,效能越好,但資料出錯的風險越高。
常見的併發問題有三種,它們會在不同的隔離等級下被解決:
1. 髒讀 (Dirty Read / Uncommitted Read)
一個交易讀取到另一個還沒提交的交易修改過的資料。如果那個未提交的交易後來取消了(Rollback),那麼第一個交易讀到的資料就是「髒」的、根本不存在的。
情境: 小明在 ATM 轉帳 1000 元給小華,但還沒按確定(未提交)。此時小華去查詢餘額,發現多了 1000 元。結果小明發現轉錯了,取消交易。小華再查,錢又不見了!
2. 不可重複讀 (Non-Repeatable Read)
一個交易在同一個事務中,多次讀取同一筆資料,結果前後不一致。這是因為在第一次讀取之後、第二次讀取之前,另一個已提交的交易修改了這筆資料。
情境: 小華查詢自己的帳戶餘額是 5000 元。此時小明成功轉帳 1000 元給小華(已提交)。小華再次查詢餘額,變成 6000 元。同一筆交易中,兩次讀取結果不同。
3. 幻影讀 (Phantom Read)
一個交易在同一個事務中,多次查詢符合相同條件的「一組」資料,結果前後不一致。這是因為在第一次查詢之後、第二次查詢之前,另一個已提交的交易新增或刪除了符合條件的資料。
情境: 小華查詢所有餘額大於 5000 元的帳戶列表,看到 5 個。此時小明新開了一個餘額 10000 元的帳戶(已提交)。小華再次查詢所有餘額大於 5000 元的帳戶列表,看到 6 個。同一筆交易中,查詢到的「資料數量」不同。
四種隔離等級,從最寬鬆到最嚴格
不同的資料庫系統(例如 MySQL、PostgreSQL、SQL Server)雖然實作上會有些微差異,但普遍都支持標準 SQL 定義的四種隔離等級:
1. Read Uncommitted (讀取未提交資料) - 最低隔離
- 允許的問題: 髒讀、不可重複讀、幻影讀。
- 特性: 一個交易可以看到其他未提交交易所做的修改。這提供了最高的併發性,但資料一致性風險也最高。通常只在對資料正確性要求極低,且追求極致速度的場景下使用。
實務場景: 幾乎不用!非常危險。
2. Read Committed (讀取已提交資料) - 避免髒讀
- 允許的問題: 不可重複讀、幻影讀。
- 特性: 一個交易只能看到其他已提交交易所做的修改。這是很多資料庫(例如 PostgreSQL、SQL Server、Oracle)的預設隔離等級。
- 解決: 髒讀。
實務場景: 大部分網路應用程式的預設選擇,在效能和資料一致性之間取得不錯的平衡。
3. Repeatable Read (可重複讀) - 避免不可重複讀
- 允許的問題: 幻影讀。
- 特性: 一個交易在讀取資料時,會對所有讀取到的資料加上共享鎖(Shared Lock),確保在該交易結束前,這筆資料不會被其他交易修改。這保證了在同一個交易中,多次讀取同一筆資料的結果都是一致的。
- 解決: 髒讀、不可重複讀。
- 注意: MySQL 的
Repeatable Read透過多版本併發控制 (MVCC) 機制,其實也能避免大部分的幻影讀問題。這點是 MySQL 和標準 SQL 定義的差異,也是一個常考的知識點。
實務場景: 需要確保在特定交易中資料讀取一致性高的報表生成、複雜的業務邏輯處理等。MySQL 的預設值。
4. Serializable (可序列化) - 最高隔離
- 允許的問題: 無。
- 特性: 這是最高的隔離等級,保證所有交易都是序列化執行(彷彿一個接一個執行),完全避免了髒讀、不可重複讀和幻影讀。它會對讀取和寫入的資料都加鎖,強制交易串行執行。
- 解決: 所有併發問題。
- 代價: 併發性最低,效能最差。因為會加很多鎖,很容易造成死鎖(Deadlock)。
實務場景: 對資料一致性有極端要求,且併發量不高的關鍵業務。但因為效能犧牲太大,極少作為系統預設值。
實務上怎麼選?
隔離等級的選擇,是效能與資料一致性之間的權衡。
大部分應用 (網頁、API):
Read Committed是一個很常見且合理的選擇。它解決了最危險的髒讀問題,同時保有不錯的併發性。對於不可重複讀和幻影讀,通常會在應用程式層面用其他方式處理(例如:重新讀取最新資料、使用樂觀鎖/悲觀鎖)。MySQL 應用: 由於 MySQL 預設是
Repeatable Read,而且它對幻影讀的處理方式也相對完善,所以很多 MySQL 開發者會直接用預設值。但如果你不熟悉 MVCC 機制,仍需注意潛在的併發問題。需要嚴格一致性: 如果你的業務邏輯真的非常複雜,對資料一致性要求極高,甚至每個查詢都必須保證資料不變,那可能才會考慮
Repeatable Read或Serializable。但這時候,你一定要仔細評估其帶來的效能衝擊和鎖競爭問題。設定方式: 大部分資料庫允許你設定全域的隔離等級,也可以在單一會話(Session)或單一交易中臨時設定:
1
2
3
4
5
6
7
8
9
10
11-- 設定全域隔離等級 (需要重啟資料庫服務才會生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 設定當前會話的隔離等級
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 在單一交易中設定隔離等級 (通常放在 BEGIN TRANSACTION 之前)
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
-- 你的 SQL 語句
COMMIT;
小結
資料庫交易與隔離等級,是每個後端工程師都應該透徹理解的核心概念。它們不是抽象的理論,而是實實在在影響你系統穩定性、資料正確性、甚至使用者體驗的基石。
理解 ACID 特性,以及四種隔離等級分別解決了哪些併發問題,可以幫助你在設計系統架構、撰寫業務邏輯,甚至除錯的時候,能更精準地找出問題點,並做出正確的技術決策。下次遇到資料不一致的問題,別忘了回來看看這篇文章,也許就能找到答案!
希望這次的白話拆解,能讓大家對資料庫交易有更清楚的認識。有任何問題或想法,都歡迎在下面留言跟我分享喔!
如果您喜歡我寫的文章,幫我按個5下讚吧!感謝您的鼓勵和支持!
![[後端] 資料庫交易與隔離等級白話講:為什麼你轉帳不能只做一半?](/img/covers/database-transaction-isolation-level-explained.jpg)
![[AI] Prompt 的眉角:讓你的 LLM 不再雞同鴨講](/img/covers/llm-prompt-techniques-practical-tips.jpg)
![[AI] 把 token 當成錢來算,context window 不是越大越好](/img/covers/token-cost-context-window.jpg)
![[學習筆記] 技術筆記要寫給未來的自己看](/img/covers/technical-note-habit.jpg)
![[後端] Log 不只是用來看錯誤](/img/covers/logging-basic.jpg)