數據庫的事務(Transaction)處理技術是很重要的概念,下面結合MySQL講講自己對這類概念的理解。
一、事務的基本概念
所謂事務是用戶定義的、不可分割的一組操作序列,這些操作只能全做或全都不做,不能存在中間狀態。涉及到用戶定義,MySQL為我們提供了三種定義事務的語句:
start transaction | begin # 開始一個新事務 commit # 提交當前事務,並將修改持久化到數據庫 rollback # 回滾當前事務,取消所有操作
而MySQL初始設置會將autocommit(自動提交)設置為1,表示用戶對數據庫的每個操作都作為一個事務自動提交,僅當使用 start transaction 或 begin 開始一個新事務后,autocommit才會臨時失效,直到出現事務結束語句(commit | rollback)。(在事務未結束之前開啟另一個新事務會自動commit)
注意:DDL語句是不能回滾的,包括create、drop、alter等。
二、事務的四個特性
事務具有4個特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability),簡稱ACID特性。
原子性:事務作為數據庫的邏輯工作單位,是不可分割的,事務內的操作要么全做,要么全不做。
一致性:事務的執行結果必須使數據庫從一個一致性狀態轉換到另一個一致性狀態。一致性是和原子性密切相關的特性,一致性狀態的轉換必然要經過原子性的操作。若數據庫在完成事務時突然發生故障而中斷,原子性不能保證,那么數據庫內的數據就會出錯,處於不一致的狀態。
隔離性:事務的執行不能互相干擾,即一個事務內的操作和使用的數據對其他並發事務是隔離的,不可見的。事務將數據庫由一個一致性狀態轉換為另一個一致性狀態時,不能出現非本事務內的操作或數據,即不能被其他事務干擾。
持久性:一個事務一旦提交,它對數據庫的修改應該是永久性的,不會因接下來的操作或故障影響已提交的事務。
事務是恢復和並發控制的基本單位,要保證事務的正常就一定要保證事務的ACID特性。事務的ACID特性被破壞可能有以下的原因:
- 多個事務並行執行,不同事務的操作交叉執行。
- 由於外部因素數據庫異常停止,導致事務在運行過程中被強制中斷。
第2個因素會導致事務的原子性遭到破壞,進而數據庫處於不一致的狀態,避免這類問題是數據庫恢復機制的責任,這里暫時不提。第1個因素則會破壞事務的原子性、一致性和隔離性,避免這類問題也就是接下來要說的並發控制的責任。
三、數據庫的並發控制
3.1 並發帶來的問題
考慮一個超市的兩台收銀機A、B的活動序列:
- A讀出當天營業額X(事務A)
- B讀出當天營業額X(事務B)
- A賣出價值為a的商品,修改營業額為X+a,並寫回數據庫(事務A完成)
- B賣出價值為b的商品,修改營業額為X+b,並寫回數據庫(事務B完成)
不考慮ACID,最終的營業額是X+b,A的修改丟失,導致事務A處於不一致的狀態,這種不一致的狀態是由並發操作引起的。
並發帶來的問題可以分為三種:1. 丟失修改;2.不可重復讀;3.讀“臟”數據。丟失修改也叫臟寫,MySQL的所有隔離級別都不允許臟寫。下面看其余兩種問題。
3.2 讀“臟”數據
也稱臟讀。在解釋臟讀前,建議先看一下關於InnoDB的臟頁刷新機制:MySQL中InnoDB臟頁刷新機制Checkpoint
簡單來說,寫回磁盤的數據可能是事務提交后的數據,也可能是事務進行中未提交的數據。每次寫回磁盤都會建立一個檢查點CheckPoint,內存中未寫回磁盤的為臟頁,事務未提交的數據是臟數據。
下面模擬一下MySQL的臟讀。打開兩個命令行窗口,並連接到MySQL,運行show processlist發現兩個進程分別對應兩個窗口的session,我們用這兩個窗口模擬並發執行事務的情況。

首先設置窗口的隔離級別為READ UNCOMMITTED(讀未提交)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
新建一張表my_table,包含兩個int類型的列id、cost,id作為主鍵,插入一條數據,然后按以下順序執行:
- A、B兩個窗口開始事務(begin)
- A查詢my_table,得到原始數據
- B修改my_table的數據,不提交
- A再查詢my_table,得到B未提交的數據
MySQL解決這個問題的方法是設置隔離級別為READ COMMITTED(讀已提交)。
3.3 不可重復讀
不可重復讀就是A事務在讀取某個數據后,事務B對其進行的修改會讓事務A在事務未結束之前讀出來,導致了A事務兩次讀取同一字段時數據不同,數據庫狀態不一致。
SQL中對數據的修改分為更新、插入和刪除,分別對應三種不可重復讀的問題,后兩種問題也被稱為“幻讀”。
理解幻讀首先要理解兩個概念:快照讀和當前讀。可以參考這篇文章:https://blog.csdn.net/silyvin/article/details/79280934
3.3.1 第一類不可重復讀
首先設置MySQL的隔離級別為READ COMMITTED(讀已提交)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
然后按以下順序執行:
- A、B兩窗口分別開始事務(begin)
- A查詢my_table,取得原數據
- B修改這條記錄,並commit(commit前A查詢出的還是原始數據)
- 再查詢這條記錄時,發現B修改后的記錄
MySQL解決這個問題的方法是設置隔離級別為REPEATABLE READ(可重復讀)。
3.3.2 第二類不可重復讀(幻讀)
幻讀也是一種不可重復讀的問題,只是側重於記錄的新增和刪除(第一類側重於修改)。
首先設置MySQL的事務隔離級別為READ COMMITTED(讀已提交)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
然后按以下順序執行復現(注意id為自增主鍵):
- A、B兩窗口分別開始事務(begin)
- A、B分別查詢my_table,得到原始數據
- A修改表的全部cost為10,同時B也插入一條cost為13的數據。
- A、B提交后,A再查詢,發現還有一條沒有修改成功(B新插入的一條)
但是用REPEATABLE READ是不是就完全解決了幻讀問題呢?並不是,可重復讀級別只能解決一部分幻讀問題,還有另一種問題會出現。
設置MySQL的事務隔離級別為REPEATABLE READ(可重復讀)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
按以下順序復現可重復讀的幻讀問題:
- A、B兩窗口分別開始事務(begin)
- A、B分別查詢my_table,得到原始數據
- A使用insert into my_table(cost) values (11)插入一條記錄,並自動順序生成id,且因為讀已提交,B不能看到A的插入記錄
- B也使用上述語句插入一條數據,發現id和前面差了2,好像前面已經有了一條數據。
- 當B想補上之前的id,發現被阻塞,不能插入,好像在修改一條臟數據。
刪除記錄的復現也類似,表現為A已刪除一條記錄並提交事務,但是B還是可以查到已刪除的記錄,而且B不能刪掉,像是出現幻覺一樣。

(左側為A事務,右側為B事務)
這類幻讀問題的解決是對查詢的數據加鎖(select * from my_table where id > 2 for update),這樣B可以鎖住id大於2的記錄的插入,A只有等待B事務提交后才可成功插入記錄。
MySQL完全解決幻讀問題的方法是設置隔離級別為SERIALIZABLE(串行化)。但是串行化對數據庫的並發性能影響較大,一般不采用這個級別的隔離。
四、總結
MySQL提供了4種隔離級別,分別為
- READ UNCOMMITTED - 讀未提交,事務內可讀到其他事務未提交的修改
- READ COMMITTED - 讀已提交,事務內可讀到其他事務已提交的修改
- REPEATABLE READ - 可重復讀(默認級別),在一個事務內重復讀數據會一直保持一致,在本事務結束后才能讀到其他事務的修改
- SERIALIZABLE - 串行化,事務不可以交替修改數據
