事務處理是保證資料安全的重要機制,事務有四個重要屬性 ,根據它們的英文名稱可以記為ACID:
- 原子性(Atomic): 事務操作是不可分割的; 事務只存在已執行和未執行兩種狀態,不存在只執行了部分指令的情況
- 一致性(Consistency): 資料庫總是從一個一致的狀態轉換到另一個一致狀態
- 隔離性(Isolation): 同時執行的事務之間相互隔離,不會互相影響。
- 永續性(Durability): 事務成功提交後, 其寫入的資料直到被覆蓋永久有效
我們以銀行轉賬操作為例理解事務:
START TRANSACTION;
UPDATE account_balance SET balance = balance - 200.00 WHERE customer_id = 1;
UPDATE account_balance SET balance = balance + 200.00 WHERE customer_id = 2;
COMMIT;
上述事務執行前後資料庫只可能有兩種狀態: 賬戶1、2的餘額未變化, 賬戶1餘額減少200元賬戶2餘額增加200元。不可能存在賬戶1餘額減少而賬戶2餘額不變的狀態。
減少賬戶1餘額和增加賬戶2餘額是一個連續的過程, 不允許在事務執行過程中對賬戶1、2餘額進行其它操作。
事務的原子性體現在兩方面:
- 事務執行過程中不允許插入其它操作。 減少賬戶1餘額和增加賬戶2餘額是一個連續的過程, 不允許在事務執行過程中對賬戶1、2餘額進行其它操作。
- 事務中的所有更改要麼都發生要麼都不發生, 不存在部分完成的情況。 減少賬戶1餘額和增加賬戶2餘額要麼都發生,要麼都不發生
事務一致性體現在: 事務執行前後資料庫總是維持在一致狀態, 轉賬開始前到轉賬結束(無論轉賬成功或失敗)的整個過程中, 賬戶1、2的總餘額始終不變。
事務隔離性體現在: 在轉賬事務減少賬戶1餘額後提交之前,另一個事務查詢到的賬戶1餘額仍是減少之前的。
併發事務的潛在問題
髒讀: 事務A修改了一個資料,但未提交,事務B讀到了事務A未提交的更新結果(即髒資料)。 如事務A在執行轉賬操作,從轉出賬戶扣除了餘額但未修改轉入賬戶餘額,此時事務B讀取了轉入賬戶的餘額, 即發生了髒讀。
不可重複讀: 在同一個事務中,對於同一條資料兩次查詢讀到的結果不一致。比如,在事務A兩次查詢中間事務B修改了某條記錄,那麼事務A兩次查詢會讀到不同的結果。
幻讀: 在同一個事務中,對於同一個查詢返回的記錄數不一致。造成這種現象的原因是在事務A的兩次查詢中間事務B新增或刪除了記錄,導致事務A兩次查詢讀到不同的結果。
幻讀和不可重複讀的區別在於,不可重複讀是對已存在記錄的修改導致的只需要對某一條記錄加鎖即可,幻讀增刪記錄導致的必須對全表加鎖。
事務隔離級別
MySQL提供四級事務隔離級別:
Read Uncommitted
: 禁止多個事務同時修改同一條記錄,其它事務可以讀取未提交的修改。 隔離級別最低,併發效能最高,會出現髒讀,不可重複讀和幻讀。Read Committed
: 禁止多個事務同時修改同一條記錄, 修改在提交前其它事務只能讀取修改前的版本。不會出現髒讀,但會出現不可重複讀和幻讀。Repeated Read
: 禁止多個事務同時修改同一條記錄, 事務提交前會鎖定所有讀取到的行,禁止其它事務修改它正在讀取的行。預設隔離級別,不會出現髒讀和不可重複讀,但會出現幻讀。Serializable
: 序列化執行,會鎖定所有涉及的資料表。可以解決髒讀、不可重複讀和幻讀, 隔離級別最高,併發效能最低。
在實際應用中我們需要根據需要選擇合適的事務隔離級別。
SET TRANSACTION
語句可以設定事務隔離級別:
-- SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 設定所有新連線的事務隔離級別
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 設定當前連線的事務隔離級別
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 設定下一個事務的隔離級別
事務併發控制原理
我們通常有3種思路進行併發控制:
悲觀鎖: 在事務進行過程中資料總是處於被鎖定狀態。
悲觀鎖對資料被其它事務修改的可能性持悲觀態度(傾向於可能發生), 常用於資料爭用激烈的情景。我們通常使用的鎖即是悲觀鎖。樂觀鎖: 在事務執行過程中資料不被鎖定, 在事務提交時會對是否發生資料爭用進行判斷,若未發生衝突則完成提交, 否則回滾事務。
樂觀鎖認為資料爭用發生的可能性較小, 常用於資料爭用比較少的情景。CAS原語是樂觀鎖的一個典型示例。快照: 所有對資料的修改都是在原有資料上產生了一個新的版本, 對資料的讀取是在快照(歷史版本)上進行的。寫操作產生新的版本不會影響在舊版本執行的讀操作。
MySQL預設使用的InnoDB儲存引擎使用悲觀鎖和快照(多版本併發控制, Multi Version Concurrent Control, MVCC)來實現事務的併發控制。
InnoDB採用兩階段鎖協議, 即事務分為擴張階段和收縮階段, 擴張階段只允許加鎖不能釋放鎖, 收縮階段只能釋放鎖不能加鎖。
MVCC
InnoDB提供了全域性唯一且有序的事務序列號, 以修改資料的事務序列號作為資料版本號。併為每條記錄維護兩個版本號: 最近修改版本號, 刪除事務號。
以 REPEATABLE READ 隔離級別下 MVCC 機制為例:
SELECT: 被檢索的行必須同時滿足兩個條件:
行的修改版本號必須小於或等於當前事務序列號
行的刪除版本號為空或者大於當前事務序列號
當前事務讀取到的資料總是事務開始前的版本或事務進行中修改的版本, 更晚開始的事務的修改不會被讀取。這種讀取方式稱為快照讀。
- INSERT: 將當前事務序列號作為修改版本號
- UPDATE: 插入一行新的記錄並使用當前事務序列號作為修改版本號, 並將當前事務序列號作為舊記錄的刪除事務號(標記為已刪除)。
DELETE: 將當前事務序列號作為記錄的刪除事務號(標記為已刪除)。
因為快照讀不會讀取到更晚開始事務的修改, 因此不會產生不可重複讀和幻讀的問題。
在不同事務隔離級別下,快照讀的一致性是不同的:
- READ COMMITTED: 每次SELECT時生成快照。SELECT 可以看到其它已提交事務的修改
- REPEATABLE READ: 事務開始時生成快照,事務內的更改會修改快照,SELECT 語句看不到其它事務的修改。
GAP 鎖
快照讀的缺陷在於只能讀取事務開始前的版本, 而對於修改操作而言必須讀取最新版本。
讀取最新版本的需求被InnoDB稱為當前讀(Locking Read), 使用當前讀的語句包括 UPDATE, DELETE 和 SELECT ... IN SHARE MODE, SELECT ... FOR UPDATE。
InnoDB使用鎖來解決當前讀的問題, InnoDB 中存在三種行級鎖:
- Record Lock: 單條行記錄上的鎖
- Gap Lock:間隙鎖,鎖定一個範圍,但不包括記錄本身
- Next-Key Lock: Record Lock + Next-Key Lock
用一個示例來說明GAP鎖:
1> START TRANSACTION;
1> DELETE FROM user WHERE age < 18;
在執行 DELETE 語句時 GAP LOCK 鎖定了所有 age < 18 的行。我們在另一個會話中開始另一個事務, 此時事務1尚未提交:
2> START TRANSACTION;
2> INSERT INTO user (age) VALUES (17);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
可以看到事務2等待鎖超時, 在事務1釋放 GAP LOCK 之前不能插入 age < 18 的行, 原有的 age < 18 的行也無法修改。
InnoDB 鎖定索引而非鎖定資料行, BTREE索引是有序的。GAP LOCK 鎖定了索引樹中 age < 18 的空間(即索引間的空隙), 被鎖定的區間不能插入記錄也不能修改已有記錄。
在 REPEATABLE READ 隔離級別下不出現幻讀是 InnoDB 儲存引擎的特性不是 MySQL 的要求, 在使用其它儲存引擎時仍可能出現幻讀問題。
更多關於讀一致性的內容可以參考 InnoDB 官方文件: innodb-consistent-read