一、基本概念
- 事務是數據庫區別於文件系統的重要特性之一,當有了事務,就可以讓數據庫始終保持
一致性
,同時可以通過事務的機制恢復到某個時間點
,保證了提交到數據庫的修改不會因爲系統崩潰而丟失; - 事務只是一個改變,是一些操作的集合,用專業術語說,就是一組
邏輯操作單元
。事務本身不具備四個特性,而是通過某些手段儘可能讓執行單元
滿足四個特性,那麼此時稱之爲一個完整的事務。
經常在網絡上、現實中一提到事務就綁死ACID四個特性,但實際ACID不僅在事務中有體現,還可以涵蓋各個領域。
二、事務處理的原則
所有的事務都是作爲一組邏輯操作單元
來執行,即使出現故障,都不能改變這種方式。當在一個事務中執行多個操作時,要麼事務提交(commit),所有修改都會永久
地保存下來;要麼事務回滾(rollback),所有修改都放棄,一切迴歸最初狀態。
常見例子:
A給B轉賬,A餘額減少,B餘額增加,這是一套必須同時完成的操作,如果其中一個失敗了,則所有操作都會失敗。
在SQL中體現爲:
UPDATE account SET money = money - 100 WHERE name = 'A';
UPDATE account SET money = money + 100 WHERE name = 'B';
這兩句SQL語句就必須同時完成,或同時失敗。
三、ACID
-
原子性(atomicity):
指事務是不可分割的工作單位,要麼全部提交,要麼全部回滾。原子就是物理世界中最小的單位了,無法在繼續分下去了,表達的是一個整體性的概念。
-
一致性(consistency):
在事務執行前後,數據從一個合法性狀態
轉換到另一個合法性狀態
,這種狀態是語義上
的,不同的業務場景,對於合法性狀態
有不同的定義。可以理解爲
滿足預定結果
的狀態稱爲合法的狀態,這種狀態由自己定義,比如現實世界中的約束。如果滿足這種狀態,那就是滿足一致性,如果不滿足,這操作失敗,事務回滾。比如餘額字段設置爲無符號數值,即錢是不允許負數的,如果此時SQL語句修改餘額小於0,此時就不滿足定義的語義,所以SQL會執行失敗(顯示出來就是報錯),事務就會回滾。
-
隔離性(isolation):
指事務的執行
不能被其它事務
干擾,即一個事務內部的操作及使用的數據對併發
的其它事務都是隔離的、獨立的,併發執行的各個事務之間不能互相干擾。比如事務A把餘額從0改成100了,此時事務A還沒有commit,而事務B進來讀取餘額了,此時結果是0還是100?這個時候就引出
隔離級別
這個概念了。 -
持久性(durability):
指事務一旦提交(commit),對數據的改變就是永久性的,之後的其它操作丟不會對其有任何影響。
比如A給B轉賬100,A減去100,B增加100,然後事務提交,A與B的餘額就都是修改後的了,如果此時A反悔不轉錢了,是無法做到的,只能再開個事務讓B給A轉回去。
持久性是通過
事務日誌
來保證的,日誌包括重做日誌(redo)
、回滾日誌(undo)
。當通過事務對數據進行修改時,首先會將數據庫的變化信息記錄到redo日誌中,然後再對數據庫對應的行進行修改,這樣的話即使數據庫崩潰,重啓後也能找到沒有更新到數據庫系統的redo日誌重新執行,從而使事務具有持久性
。
總結:在ACID中,原子性是基礎,隔離性是手段,一致性是約束條件,持久性是最終的目的。
如果要一句話回答事務是什麼?那就是保證原子性
、一致性
、隔離性
,持久性
的一個或多個數據庫操作稱爲一個事務。
四、事務的狀態
-
活動的(active):
事務對應的數據庫操作正在執行的過程中,稱爲事務處在
活動的
狀態。 -
部分提交的(partially committed):
當事務的
最後一個操作
執行完成,改動的數據的還處在內存中,並沒有刷寫到磁盤
,此時稱爲事務處在部分提交的
狀態。 -
失敗的(failed):
當事務處於
活動的
或者部分提交的
狀態時,可能遇到某些錯誤(數據庫自身錯誤、操作系統錯誤、斷電等)而無法繼續執行,或人爲的停止事務的執行,稱爲事務處於失敗的
狀態。 -
中止的(aborted):
如果事務執行了一部分而變爲
失敗的
狀態,那麼就需要將修改過的事務的操作還原(就是回滾操作),還原完成後,事務就出在了中止的
狀態。 -
提交的(committed):
處於
部分提交的
狀態的事務將修改過的數據
刷寫到磁盤後,稱爲事務處在提交的
狀態。
五、事務的隔離級別
事務的隔離級別
的出現,就是爲了解決ACID中的 I(isolation 隔離性)在併發環境下,同時被操作的數據的隔離問題,同前面所述例子:
比如事務A把餘額從0改成100了,此時事務A還沒有commit,而事務B進來讀取餘額了,此時結果是0還是100?
以MySQL爲例,MySQL是一個客戶端/服務端
架構的軟件,對於同一個服務器來說,可以由若干個客戶端與之連接,每個客戶端與服務端連接上之後,稱爲一個會話(Session)。每個客戶端都可以在自己的會話中向服務器發出請求語句,而請求語句可以是某個事務的一部分。對於服務器來說可以同時處理多個事務,但又要滿足隔離性
的特性,理論上在某個事務對某個數據進行訪問
時,其它事務應該進行排隊
,直到該事務提交之後,其它事務才能繼續訪問這個數據(類比Java多線程鎖機制),但這樣性能影響太大。如果既要保持事務的隔離性,又想讓服務器在處理訪問同一數據
的多個事務的性能儘可能高,這時候就看二者如何權衡取捨了。
5.1 準備數據
創建一張表及數據
CREATE TABLE student (
no INT,
name VARCHAR(20),
age INT,
PRIMARY KEY (no)
) Engine=InnoDB;
INSERT INTO student VALUES(1, '小明', 18);
確認數據正常:
mysql> SELECT * FROM student;
+----+--------+------+
| no | name | age |
+----+--------+------+
| 1 | 小明 | 18 |
+----+--------+------+
1 row in set (0.00 sec)
5.2 數據併發問題
在併發情況下,如果不保證串行執行(就是一個接一個執行),那麼會出現以下幾種問題:
1. 髒寫(Dirty Write)
對於兩個事務 Session A、Session B,如果 Session A修改
了 Session B 修改過
但還未提交
的數據,就意味着發生了髒寫。
發生時間編號 | Session A | Session B |
---|---|---|
① | BEGIN; | |
② | BEGIN; | |
③ | UPDATE student SET name = '李四' WHERE no = 1; |
|
④ | UPDATE student SET name = '張三' WHERE no = 1; |
|
⑤ | COMMIT; | |
⑥ | ROLLBACK; |
已知表中僅有一條no爲1、name爲小明
的記錄。
Session A 與 Session B 各自開啓自己的事務,Session B 的事務先將no爲1的記錄修改name列的值爲李四
,然後這條記錄緊接着又 Session A 給改成了張三
,並且Session A 還 COMMIT 了,按理來說
此時張三
就應該永久刷寫到磁盤了,但接着Session B 將它的事務回滾了,對於這記錄的修改全部撤回,即no爲1的記錄的name列的值爲小明
。那對於 Session A 來說就有問題了,明明update且commit了,最後一看什麼變化都沒有,這是無法被容忍的問題,所以在標準SQL中的四種隔離級別中都解決了髒寫
的問題(就是沒法復現了)。
2. 髒讀(Dirty Read)
對於兩個事務 Session A、Session B,如果 Session A讀取
了 Session B 修改過
但還未提交
的數據,之後 Session B 回滾,則 Session A 讀到的數據就是臨時且無效
的。
發生時間編號 | Session A | Session B |
---|---|---|
① | BEGIN; | |
② | BEGIN; | |
③ | UPDATE student SET name = '李四' WHERE no = 1; |
|
④ | SELECR * FROM student WHERE no = 1; (如果此時讀出來是 李四 ,而不是小明 ,則發生了髒讀) |
|
⑤ | COMMIT; | |
⑥ | ROLLBACK; |
已知表中僅有一條no爲1、name爲小明
的記錄。
Session A 與 Session B 各自開啓自己的事務,Session B 的事務先將no爲1的記錄修改name列的值爲李四
,然後緊接着 Session A 查詢這條記錄,如果發現讀到的數據爲李四
,即讀到了Session B 中還沒有提交
的數據,之後 Session B 進行了回滾,數據還原回小明
,那麼 Session A 讀到的李四
就是一個不存在的數據,這種現象稱爲髒讀
。
在某些場合下,返回不存在的數據、假的數據給客戶,是可能會出大問題的。
3. 不可重複度(Non-Repeatable Read)
對於兩個事務 Session A、Session B,Session A 讀取了一條記錄的一個字段,然後 Session B 修改
了這條記錄的同個字段,接着 Session A 再重複上一次查詢,兩次結果不同了
,這就發生了不可重複讀。
發生時間編號 | Session A | Session B |
---|---|---|
① | BEGIN; | |
② | SELECR * FROM student WHERE no = 1; (此時讀出來是 小明 ) |
|
③ | BEGIN; UPDATE student SET name = '李四' WHERE no = 1; COMMIT; |
|
④ | SELECR * FROM student WHERE no = 1; (讀出來是 李四 ,跟上次不同,則發生了不可重複讀) |
|
⑤ | BEGIN; UPDATE student SET name = '王五' WHERE no = 1; COMMIT; |
|
⑥ | SELECR * FROM student WHERE no = 1; (讀出來是 王五 ,又跟上次不同,則發生了不可重複讀) |
已知表中僅有一條no爲1、name爲小明
的記錄。
Session A 第一次查詢該記錄爲小明
(注意還沒有COMMIT),接着 Session B 修改該記錄爲李四
,然後 Session A 再次查詢發現變成李四
了,跟上一次查詢結果不同,這種現象稱爲不可重複讀
。同理 Session B 又修改值爲王五
,緊接着Session A 又再次查詢發現結果又變了,變成了王五
,因爲發生了不可重複讀
。
對於這種現象,在現實中很多場景下中我們會認爲這是合理的、可以接受的,每次查詢就應該查出當前最新的數據,但處於併發事務內的角度來看這屬於一種問題。
4. 幻讀(Phantom Read)
對於兩個事務 Session A、Session B,Session A 讀取了一條記錄的一個字段,然後 Session B 往表插入
一些新的記錄,之後 Session A 再次讀取
發現多出了新的記錄,這就算幻讀。
發生時間編號 | Session A | Session B |
---|---|---|
① | BEGIN; | |
② | SELECR * FROM student WHERE no > 0; (此時讀出來只有 小明 一條記錄) |
|
③ | BEGIN; INSERT INTO student VALUES(2, '李四', 19); COMMIT; |
|
④ | SELECR * FROM student WHERE no > 0; (此時讀出來有 小明 、李四 兩條記錄,發生了幻讀) |
已知表中僅有一條no爲1、name爲小明
的記錄。
Session A 第一次查詢限定條件爲 no > 0
,查出來一條記錄爲小明
(注意還沒有COMMIT),接着 Session B 插入了一條記錄李四
,然後 Session A 再次查詢結果爲小明
、李四
兩條記錄了,多了一條,這種現象稱爲幻讀
,而多出來的記錄稱爲幻影記錄
。
對於這種現象,還是那句話,在現實中很多場景下中我們會認爲這是合理的、可以接受的,但處於併發事務內的角度來看這屬於一種問題。
對於幻讀的注意點:
幻讀強調的是多出來的
記錄。按照示意圖,如果 Session B 不是插入記錄,而是刪除記錄,那 Session A 再次查詢發現記錄數量變少了,這種現象不屬於幻讀。由於第二次查變少了,嚴格歸類的話,這種現象屬於不可重複讀
。
5.3 SQL標準的四種隔離級別
按照併發問題嚴重程度高到低排序:髒寫
> 髒讀
> 不可重複讀
> 幻讀
由於髒寫
問題是無法接受的,所以現在市面主流數據庫都不會出現這個問題,那還剩下後面3個問題。如果能把所有問題都解決那肯定最好,但也就意味着併發性能最差,如果要最高的併發性能,又會出現數據併發的問題......因此回到前面說的權衡取捨問題了,看不同的場景是要數據絕對準確性
,還是要最高的併發性能
,亦或犧牲部分併發性能以換取數據的相對準確
。
在SQL官方標準中,設立了以下四個隔離級別:
1. 讀未提交(Read Uncommitted)
在該隔離級別中,所有事務都可以看到其它未提交事務
的執行結果,意味着會發生髒讀
、不可重複讀
,幻讀
。
2. 讀已提交(Read Committed)
讀已提交滿足了隔離的基本定義:一個事務只能看見已經提交的事務
的執行結果。這是大部分數據庫系統的默認隔離級別,比如Oracle、SQL Server,可以避免髒讀
,但仍會發生不可重複讀
,幻讀
。
3. 可重複讀(Repeatable Read)
事務A在讀到一條數據之後,此時事務還沒提交或回滾,接着事務B對該數據進行了修改並提交,那麼事務A再次讀取該數據,讀到的還是原來的內容。可以避免髒讀
、不可重複讀
,但仍會發生幻讀
,這是MySQL默認的隔離級別。
4. 可串行化(Serializable)
確保事務可以從一個表中讀取相同的行,在整個事務持續期間,禁止其它事務對該表相同行進行插入、更新,刪除操作。所有的問題都可以解決,但性能最爲低下。
髒寫 | 髒讀 | 不可重複讀 | 幻讀 | 加鎖讀 | |
---|---|---|---|---|---|
Read Uncommitted | × | √ | √ | √ | × |
Read Committed | × | × | √ | √ | × |
Repeatable Read | × | × | × | √ | × |
Serializable | × | × | × | × | √ |
5.4 不同數據庫的隔離級別
SQL標準雖然有4種隔離級別,但不同的數據庫的支持程度不一樣。
Oracle | MySQL | SQL Server | |
---|---|---|---|
Read Uncommitted | × | √ | √ |
Read Committed | √(默認) | √ | √(默認) |
Repeatable Read | × | √(默認) | √ |
Serializable | √ | √ | √ |
Snapshot | √ | ||
Read Committed Snapshot | √ |
值得注意的是:
- MySQL 的實現 Repeatable Read 在一定程度上也可以解決幻讀的問題(快照讀+MVCC機制),與SQL官方標準略有區別;
- SQL Server 支持的隔離級別比SQL標準的還要多出兩種。
5.5 其它
MySQL中查看隔離級別:
# 5.7.20之前
SHOW VARIABLES LIKE 'tx_isolation';
# 5.7.20及之後
SHOW VARIABLES LIKE 'transaction_isolation';
# 所有版本均可用
SELECT @@transaction_isolation;
MySQL中設置隔離級別:
SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔離級別';
隔離級別可選項:
> READ-UNCOMMITTED
> READ-COMMITTED
> REPEATABLE-READ
> SERIALIZABLE
或者:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL '隔離級別';
隔離級別可選項:
> READ UNCOMMITTED
> READ COMMITTED
> REPEATABLE READ
> SERIALIZABLE
GLOBAL:對後續所有新開會話有效
SESSION:只對當前會話有效
無論設置什麼級別,數據庫一旦重啓都以配置文件爲準