之前對於資料庫事務概念的理解有很多不到位的地方,今天用簡單的例項再來闡述一下資料庫事務和隔離級別的概念,也方便以後溫故而知新。
1. 什麼是事務
事務(Transaction)是併發控制的基本單位。所謂的事務,它是一個操作序列,這些操作要麼都執行,要麼都不執行,它是一個不可分割的工作單位。例如,銀行轉賬工作:從一個賬號扣款並使另一個賬號增款,這兩個操作要麼都執行,要麼都不執行。所以,應該把它們看成一個事務。事務是資料庫維護資料一致性的單位,在每個事務結束時,都能保 持資料一致性。
我們以Msql資料庫的操作為例,再進一步解釋一下資料庫事務:
首先我們用以下命令檢視該Mysql會話的事務隔離級別,關於事務隔離級別及其作用,我們在後面的章節中會進行詳細介紹,這裡只要簡單知道資料庫可以設定不同的事務隔離級別,不同的隔離級別會對事務的操作產生不同的效果即可。使用以下命令可以查詢當前Mysql會話的事務隔離級別,可以看到,Mysql預設的事務隔離級別是REPEATABLE-READ。
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
複製程式碼
為了用例項來解釋事務,我們建立瞭如下的bank資料表,並插入一條資料,
mysql> describe bank;
+---------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+---------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(40) | NO | | NULL | |
| balance | decimal(10,2) | YES | | NULL | |
+---------+---------------+------+-----+---------+----------------+
mysql> select * from bank;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 3 | fufu | 2000.00 |
+----+------+---------+
複製程式碼
使用start transaction命令開啟資料庫事務,
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
複製程式碼
更新id為3的行的balance值為3000.00,
mysql> update bank set balance = 3000 where id = 3;
Query OK, 1 row affected (0.09 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from bank;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 3 | fufu | 3000.00 |
+----+------+---------+
1 row in set (0.00 sec)
複製程式碼
此時我們可以看到,select語句查詢到的id為3的行的balance值已經修改為3000.00,接下來我們再嘗試插入一條新資料,
mysql> insert into bank (name, balance) values ('melo', 1000);
Query OK, 1 row affected (0.06 sec)
mysql> select * from bank;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 3 | fufu | 3000.00 |
| 4 | melo | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)
複製程式碼
由於以上的update和insert操作都是在start transaction命令開啟事務之後,所以直到事務結束,這些操作都屬於同一事務,假設我們在insert操作時產生了錯誤,可以根據事務的定義得知,這些屬於同一事務的所有操作要麼都執行要麼都不執行,我們可以驗證一下,使用rollback命令,模擬事務失敗回滾,
mysql> rollback;
Query OK, 0 rows affected (0.01 sec)
複製程式碼
此時我們在查詢資料庫中的所有資料,發現資料恢復到了update命令執行前的狀態,id為3的行的balance值等於2000沒有變化。
mysql> select * from bank;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 3 | fufu | 2000.00 |
+----+------+---------+
1 row in set (0.00 sec)
複製程式碼
到此,我們闡述了資料庫事務的定義並用簡單的Mysql操作說明了事務的操作方式,我們可以總結出資料庫事務的生命週期如下:
- 事務的開始邊界
- 事務的正常結束邊界(COMMIT),提交事務,永久儲存被事務更新後的資料庫狀態。
- 事務的異常結束邊界(ROLLBACK):撤銷事務,使資料庫退回到執行事務前的初始狀態。
現在我們回過頭來思考一下上述示例,示例中的所有操作都是在一個Mysql會話中進行的,也就是沒有其他使用者在同時連線資料庫進行操作,在這種沒有併發會話的使用場景中,無論事務是正常結束還是異常結束,對於該單獨使用者讀取資料不會造成任何影響,因為他的所有操作都是序列的。但是在實際應用場景中,資料庫每時每刻都服務於很多會話,假設使用者A的事務A開始後更新了資料庫資料,此時使用者B開始讀取該資料,使用者B將會讀取到了新的值。但是如果緊接著事務A在下一條SQL語句操作時產生了錯誤,將事務A回滾了,那麼使用者B讀取到的資料就是錯誤的無效資料了。這只是資料庫事務在併發環境下會產生的一個簡單的問題,所以接下來詳細闡述併發事務會產生的問題。
2. 併發事務會產生的問題
這節我們主要說明併發事務時可能會出現的問題,我們用時間點和事務操作表格的方式來舉例。
2.1 丟失更新
2.1.1 第一類丟失更新
定義:A事務撤銷時,把已經提交的B事務的更新資料覆蓋了。
時間點 | 事務A | 事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢賬戶餘額為1000元 | |
T4 | 查詢賬戶餘額為1000元 | |
T5 | 存入100元把餘額改為1100元 | |
T6 | 提交事務 | |
T7 | 取出100元把餘額改為900元 | |
T8 | 撤銷事務 | |
T9 | 餘額恢復為1000元(丟失更新) |
以上的示例演示了第一類丟失更新問題,事務B雖然成功了,但是它所做的更新沒有被永久儲存,這種併發問題是由於完全沒有隔離事務造成的。當兩個事務更新相同的資料時,如果一個事務被提交,另一個事務卻撤銷,那麼會連同第一個事務所做的更新也被撤銷了。(這是絕對避免出現的事情) 事務A的開始時間和結束時間包含事務B的開始和結束時間,事務A回滾事務的同時,把B的已經提交的事務也回滾的,這是避免的,這就是第一類丟失更新.
2.1.2 第二類丟失更新
定義:A事務提交時,把已經提交的B事務的更新資料覆蓋了。
時間點 | 事務A | 事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢賬戶餘額為1000元 | |
T4 | 查詢賬戶餘額為1000元 | |
T5 | 取出100元把餘額改為900元 | |
T6 | 提交事務 | |
T7 | 存入100元把餘額改為1100 | |
T8 | 提交事務 | |
T9 | 餘額恢復為1100元(丟失更新) |
第二類丟失更新和第一類的區別實際上是對資料的影響是由A事務的撤銷還是提交造成的,它和不可重複讀(下面介紹)本質上是同一類併發問題,通常把它看做是不可重複讀的一個特例。兩個或多個事務查詢同一資料。然後都基於自己的查詢結果更新資料,這時會造成最後一個提交的更新事務,將覆蓋其它已經提交的更新事務。
2.2 髒讀
定義:讀到未提交更新的資料
時間點 | 事務A | 事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢賬戶餘額為1000元 | |
T4 | 取出500元把餘額改為500元 | |
T5 | 查詢賬戶餘額為500元(髒讀) | |
T6 | 撤銷事務,餘額恢復為1000元 | |
T7 | 存入100元把餘額改為600元 | |
T8 | 提交事務 |
A事務查詢到了B事務未提交的更新資料,A事務依據這個查詢結果繼續執行相關操作。但是接著B事務撤銷了所做的更新,這會導致A事務操作的是髒資料,以上的示例中T5時刻產生了髒讀,最終導致A事務提交時賬戶餘額的不正確,可能有人會有疑問,B事務還沒有提交或撤銷,T5時刻A事務為什麼能讀到已經改變的資料,這裡要說的是,資料表中的資料是實時改變的,事務只是控制資料的最終狀態,也就是說如果沒有正確的隔離級別,在更新操作語句結束後,即使事務未完成,其他事務就已經可以讀取到改變的資料值了。
現在為止:所有的資料庫都避免髒讀操,可以用兩個Mysql會話試驗一下以上的操作,在預設的隔離級別下(REPEATABLE-READ),A事務在T5時刻讀取到的餘額為1000元,不會是500元。
2.3 不可重複讀
定義:讀到已經提交更新的資料,但一個事務範圍內兩個相同的查詢卻返回了不同資料。
時間點 | 事務A | 事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢賬戶餘額為1000元 | |
T4 | 查詢賬戶餘額為1000元 | |
T5 | 取出100元把餘額改為900元 | |
T6 | 提交事務 | |
T7 | 查詢賬戶餘額為900元(與T4讀取的一不一致,不可重複讀) |
2.4 幻讀
定義:讀到已提交插入資料,幻讀與不可重複讀類似,幻讀是查詢到了另一個事務已提交的新插入資料,而不可重複讀是查詢到了另一個事務已提交的更新資料。
時間點 | 事務A | 事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 統計使用者Z總存款數為1000元 | |
T4 | 新增Z的一個存款賬號,存款100元 | |
T5 | 提交事務 | |
T6 | ||
T7 | 再次統計使用者Z總存款數為1100元(與T4讀取的一不一致,幻讀) |
A事務第一次查詢時,沒有問題,第二次查詢時查到了B事務已提交的新插入資料,這導致兩次查詢結果不同。
不可重複讀和幻讀的區別:
簡單來說,不可重複讀是由於資料修改引起的,幻讀是由資料插入或者刪除引起的。
不可重複讀,是指在資料庫訪問中,一個事務範圍內兩個相同的查詢卻返回了不同資料。這是由於查詢時系統中其他事務修改的提交而引起的。比如事務T1讀取某一資料,事務T2讀取並修改了該資料,T1為了對讀取值進行檢驗而再次讀取該資料,便得到了不同的結果。
一種更易理解的說法是:在一個事務內,多次讀同一個資料。在這個事務還沒有結束時,另一個事務也訪問該同一資料。那麼,在第一個事務的兩次讀資料之間。由於第二個事務的修改,那麼第一個事務讀到的資料可能不一樣,這樣就發生了在一個事務內兩次讀到的資料是不一樣的,因此稱為不可重複讀,即原始讀取不可重複。
所謂幻讀,是指事務A讀取與搜尋條件相匹配的若干行。事務B以插入或刪除行等方式來修改事務A的結果集,然後再提交。
幻讀是指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的資料進行了修改,比如這種修改涉及到表中的“全部資料行”。同時,第二個事務也修改這個表中的資料,這種修改是向表中插入“一行新資料”。那麼,以後就會發生操作第一個事務的使用者發現表中還有沒有修改的資料行,就好象發生了幻覺一樣.一般解決幻讀的方法是增加範圍鎖RangeS,鎖定檢鎖範圍為只讀,這樣就避免了幻讀。
3. 事務隔離級別
以上就是資料庫併發事務導致的五大問題,總結來說其中兩類是更新問題,三類是讀問題,資料庫是如何避免這種併發事務問題的呢?答案就是通過不同的事務隔離級別,在不同的隔離級別下,併發事務讀取資料的結果是不一樣的,比如在髒讀小節裡介紹的,如果是在REPEATABLE-READ隔離級別下,A事務在T5時刻讀取是讀取不到B事務未提交的資料的。我們需要根據業務的要求,設定不同的隔離級別,在效率和資料安全性中找到平衡點。
SQL標準定義了4類隔離級別,包括了一些具體規則,用來限定事務內外的哪些改變是可見的,哪些是不可見的。低階別的隔離級一般支援更高的併發處理,並擁有更低的系統開銷。
3.1 SERIALIZABLE(序列化)
當資料庫系統使用SERIALIZABLE隔離級別時,一個事務在執行過程中完全看不到其他事務對資料庫所做的更新。當兩個事務同時運算元據庫中相同資料時,如果第一個事務已經在訪問該資料,第二個事務只能停下來等待,必須等到第一個事務結束後才能恢復執行。因此這兩個事務實際上是序列化方式執行。
3.2 REPEATABLE READ(可重複讀)
當資料庫系統使用REPEATABLE READ隔離級別時,一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄,但是不能看到其他事務對已有記錄的更新。
3.3 READ COMMITTED(讀已提交資料)
當資料庫系統使用READ COMMITTED隔離級別時,一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄,而且還能看到其他事務已經提交的對已有記錄的更新。
3.4 READ UNCOMMITTED(讀未提交資料)
當資料庫系統使用READ UNCOMMITTED隔離級別時,一個事務在執行過程中可以看到其他事務沒有提交的新插入的記錄,而且還能看到其他事務沒有提交的對已有記錄的更新。
以上的四種隔離級別按從高到底排序,你可能會說,選擇SERIALIZABLE,因為它最安全!沒錯,它是最安全,但它也是最慢的!四種隔離級別的安全性與效能成反比!最安全的效能最差,最不安全的效能最好!
4. 隔離級別與併發問題
通過以上的四種隔離級別的定義,我們已經可以分析出,每個隔離級別可以避免哪些併發問題了,總結一下如下表:
隔離級別 | 第一類丟失更新 | 第二類丟失更新 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|---|---|
SERIALIZABLE (序列化) | 避免 | 避免 | 避免 | 避免 | 避免 |
REPEATABLE READ(可重複讀) | 避免 | 避免 | 避免 | 避免 | 允許 |
READ COMMITTED (讀已提交) | 避免 | 允許 | 避免 | 允許 | 允許 |
READ UNCOMMITTED(讀未提交) | 避免 | 允許 | 允許 | 允許 | 允許 |
我們通過隔離級別的定義很容易自己分析出這張表,比如可重複讀隔離級別的定義是一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄,但是不能看到其他事務對已有記錄的更新。所以,在這種隔離級別下,在髒讀示例的T5時刻和不可重複讀的T7時刻,事務A都是無論事務B是否提交,事務A都是無法讀取到事務B對已有記錄的更新的,所以不會產生髒讀和不可重複讀,而又由於這種隔離級別下可以看到其他事務已經提交的新插入記錄,自然是無法避免幻讀的產生。另外,值得注意的是所有隔離級別都可以避免第一類丟失更新的問題。
大多數關聯式資料庫預設使用Read committed的隔離級別,Mysql InnoDB預設使用Read repeatable的隔離級別,這和Mysql replication 機制使用Statement日誌格式有關。各資料庫隔離級別的實現也是有差別的,例如Oracle支援Read committed 和Serializable兩種隔離級別,另外可以通過使用讀快照在Read committed級別上禁止不可重複讀問題;MySQL預設採用RR隔離級別,SQL標準是要求RR解決不可重複讀的問題,但是因為MySQL採用了gap lock,所以實際上MySQL的RR隔離級別也解決了幻讀的問題,也就是Mysql InnoDB在Read repeatable級別上使用next-key locking 策略來避免幻讀現象的產生。