想必參加事後臺開發麪試的夥伴們都知道,MySQL事務這玩意是各大面試官百問不厭的知識點,可是你們對於事務的瞭解到什麼層面呢,僅僅停留在ACID
上麼,這篇文章將陪着你們一塊兒深刻MySQL中的事務。面試
引言中所提到的ACID正是事務的四個特性:分別是原子性
(Atomicity)、一致性
(Consistency)、隔離性
(Isolation)、持久性
(Durability)sql
其中一致性不太好理解,一致性是說不管事務提交仍是回滾,不會破壞數據的完整性。好比A給B轉100元,若是成功了,A的帳戶一定會扣100元,而B的帳戶一定會增長100元;若是失敗了,A和B的帳戶餘額不會改變。A和B中的帳戶金額變更必然是一個完整的過程(不多是A扣除了50,B增長了50這種狀況),整個過程必須是一致的。數據庫
事務的原子性是指:一個事務中的多個操做都是不可分割的,只能是所有執行成功、或者所有執行失敗。
MySQL事務的原子性是經過undo log
來實現的。undo log
是InnoDB存儲引擎特有的。具體的實現機制是:將全部對數據的修改(增、刪、改)都寫入日誌(undo log
)。安全
undo log
是邏輯日誌,能夠理解爲:記錄和事務操做相反的SQL語句,事務執行insert語句,undo log就記錄delete語句。它以追加寫的方式記錄日誌,不會覆蓋以前的日誌。除此以外undo log還用來實現數據庫多版本併發控制(Multiversion Concurrency Control,簡稱MVCC)。
若是一個事務中的一部分操做已經成功,但另外一部分操做,因爲斷電/系統崩潰/其它的軟硬件錯誤而沒法成功執行,則經過回溯日誌,將已經執行成功的操做撤銷,從而達到所有操做失敗的目的。併發
事務的持久性是指:一個事務對數據的全部修改,都會永久的保存在數據庫中。
MySQL事務的持久性是經過redo log
來實現的。redo log
也是InnoDB存儲引擎特有的。具體實現機制是:當發生數據修改(增、刪、改)的時候,InnoDB引擎會先將記錄寫到redo log
中,並更新內存,此時更新就算完成了。同時InnoDB引擎會在合適的時機將記錄刷到磁盤中。函數
redo log
是物理日誌,記錄的是在某個數據頁作了什麼修改,而不是SQL語句的形式。它有固定大小,是循環寫的方式記錄日誌,空間用完後會覆蓋以前的日誌。
undo log
和redo log
並非直接寫到磁盤上的,而是先寫入log buffer
。再等待合適的時機同步到OS buffer
,再由操做系統決定什麼時候刷到磁盤,具體過程以下:既然
undo log
和redo log
都是從log buffer
到 OS buffer
,再到磁盤。因此中途仍是有可能由於斷電/硬件故障等緣由致使日誌丟失。爲此MySQL提供了三種持久化方式:這裏有一個參數innodb_flush_log_at_trx_commit
,這個參數主要控制InnoDB
將log buffer
中的數據寫入OS buffer
,並刷到磁盤的時間點,取值分別爲0,1,2,默認是1。這三個值的意思以下圖所示:
首先查看MySQL默認設置的方式1,也就是每次提交後直接寫入OS buffer
,而且調用系統函數fsync()
把日誌寫到磁盤上。就保證數據一致性的角度來講,這種方式無疑是最安全的。可是咱們都知道,安全大多數時候意味着效率偏低。每次提交都直接寫入OS buffer
而且寫到磁盤,無疑會致使單位時間內IO的次數過多而效率低下。除此以外,還有方式0和方式2。基本上都是每秒寫入磁盤一次,因此效率都比方式1更高。可是方式0是把數據先寫入log buffer再寫入OS buffer
再寫入磁盤,而方式2是直接寫入OS buffer
,再寫入磁盤,少了一次數據拷貝的過程(從log buffer
到OS buffer
),因此方式2比方式0更加高效。優化
瞭解了undo log
和redo log
的做用和實現機制以後,那麼這兩個日誌具體是怎麼讓數據庫從異常的狀態恢復到正常狀態的呢?數據庫系統崩潰後重啓,此時數據庫處於不一致的狀態,必須先執行一個
crash recovery
的過程:首先讀取redo log
,把成功提交可是還沒來得及寫入磁盤的數據從新寫入磁盤,保證了持久性。再讀取undo log
將尚未成功提交的事務進行回滾,保證了原子性。crash recovery
結束後,數據庫恢復到一致性狀態,能夠繼續被使用。spa
數據庫事務的隔離性是指:多個事務併發執行時,一個事務的執行不該影響其餘事務的執行。正常狀況下,確定是多個事務同時操做同一個數據庫,因此事務之間的隔離就顯得必不可少。
若是沒有隔離性,將會發生如下問題:操作系統
一個事務在撤銷的時候,覆蓋了另外一個事務已提交的更新數據。
假設如今有兩個事務A、B同時操做同一帳戶的金額,以下圖所示:
顯然,事務B在撤銷事務的時候,覆蓋了事務A在T4階段已經提交的更新數據。A在T3的時候已經取走了200元,此時的餘額應該是800元,可是因爲事務B開始的時候,餘額是1000元,因此回滾後,餘額也會變成1000元。這樣一來,用戶明明取了錢,可是餘額不變,銀行虧到姥姥家了。3d
一個事務讀到了另外一個事務未提交的更新數據。
用下圖說明:
事務A在T3的時候取走了200元,可是未提交。事務B在T4時查詢餘額就能看到事務A未提交的更新。
幻讀(虛讀)是指:一個事務讀到了另外一個事務已提交的新增數據。
依然是配圖說明:
事務B在同一個事務中執行兩次統計操做之間,另外一事務insert了一條記錄,致使獲得的結果不同,好像發生了幻覺。還有一種狀況是事務B更新了表中全部記錄的某一字段,以後事務A又插入了一條記錄,事務B再去查詢發現有一條記錄沒有被更新,這也是幻讀。
不可重複讀:一個事務讀到了另外一個事務已提交的更新數據。
不可重複讀,顧名思義,就是在同一個事務中重複讀取數據會發生不一致的狀況,以下圖:
事務B在T2和T5階段都執行了查詢餘額的操做,可是每次獲得的結果都不同,這在開發中是不容許的,同一個事務中一樣的屢次查詢,每次返回不同的結果,讓人難免會對數據庫的可靠性產生懷疑。
一個事務在提交的時候,覆蓋了另外一個事務已提交的更新數據。
由上圖能夠看出,當事務A提交以後,帳戶餘額已經發生了變更,而後事務B仍是基於原始金額(即1000)的基礎上扣除取款金額的,事務B以提交,就是把事務A的提交給徹底覆蓋了。此爲第二類丟失更新。
注意和第一類丟失更新區分,第一類丟失更新重點在事務B最終撤銷了事務,第二類是最終提交了事務。
爲了解決這五類問題,MySQL提供了四種隔離級別:
MySQL默認的隔離級別
,同一個事務中相同的查詢會看到一樣的數據行,安全性較高,效率較好隔離級別 | 是否出現第一類丟失更新 | 是否出現髒讀 | 是否出現虛讀 | 是否出現不可重複讀 | 是否出現第二類丟失更新 |
---|---|---|---|---|---|
Serializable | 否 | 否 | 否 | 否 | 否 |
Repeatable Read | 否 | 否 | 是 | 否 | 否 |
Read Commited | 否 | 否 | 是 | 是 | 是 |
Read Uncommited | 否 | 是 | 是 | 是 | 是 |
Repeatable Read(可重複讀)
是MySQL默認的隔離級別,也是使用最多的隔離級別,因此單獨拿出來深刻理解頗有必要。Repeatable Read
沒法解決幻讀(虛讀)問題。下面來看一個實例。
首先建立一個表並插入一條記錄:
CREATE TABLE `student` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `stu_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '學生學號', `stu_name` varchar(100) DEFAULT NULL COMMENT '學生姓名', `created_date` datetime NOT NULL COMMENT '建立時間', `modified_date` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間', `ldelete_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '邏輯刪除標誌,0:未刪除,2:已刪除', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='學生信息表'; INSERT INTO `student` VALUES (1, 230160340, 'Carson', '2016-08-20 16:37:00', '2016-08-31 16:37:05', 0);
一樣的開啓兩個事務,以下表所示:
時間 | 事務A | 事務B | |
---|---|---|---|
T1 | SELECT * FROM student | - | |
T2 | - | INSERT INTO student VALUES (2, 230160310, 'Kata', '2016-08-20 16:37:00', '2016-08-31 16:37:05', 0) | |
T3 | - | commit | |
T4 | SELECT * FROM student | - |
按照上述理論,會出現幻讀現象。也就是事務A在T4時間段的查詢select會看到事務B提交的新增數據。
但要讓你失望了。
執行結果以下
和預期的結果並不一致,沒有出現幻讀現象。
實際上MySQL在Repeatable Read
隔離級別下,用MVCC(Multiversion Concurrency Control
,多版本併發控制)解決了select普通查詢的幻讀現象。
具體的實現方式就是事務開始時,第一條select語句查詢結果集會生成一個快照(snapshot
),而且這個事務結束前,一樣的select語句返回的都是這個快照的結果,而不是最新的查詢結果,這就是MySQL在Repeatable Read
隔離級別對普通select語句使用的快照讀(snapshot read
)。
快照讀和MVCC是什麼關係?
MVCC是多版本併發控制,快照就是其中的一個版本。因此能夠說MVCC實現了快照讀,具體的實現方式涉及到MySQL的隱藏列。MySQL會給每一個表自動建立三個隱藏列:
DB_TRX_ID
:事務ID,記錄操做(增、刪、改)該數據事務的事務IDDB_ROLL_PTR
:回滾指針,記錄上一個版本的數據在undo log中的位置DB_ROW_ID
:隱藏ID ,建立表沒有合適的索引做爲聚簇索引時,會用該隱藏ID建立聚簇索引因爲undo log
中記錄了各個版本的數據,而且經過DB_ROLL_PTR
能夠找到各個歷史版本,而且由DB_TRX_ID
決定使用哪一個版本(快照)。因此至關於undo log
實現了MVCC,MVCC實現了快照讀。
如此看來,MySQL的Repeatable Read
隔離級別利用快照讀,已經解決了幻讀的問題。
可是事實並不是如此,接下來再看一個例子
時間 | 事務A | 事務B | |
---|---|---|---|
T1 | SELECT * FROM student | - | |
T2 | - | INSERT INTO student VALUES (3, 230160312, 'Luffy', '2016-08-20 16:37:00', '2016-08-31 16:37:05', 0) | |
T3 | - | commit | |
T4 | UPDATE student SET stu_name = 'Katakuri' WHERE stu_name = 'Luffy'; | - | |
T4 | SELECT * FROM student | - |
事務A在T1的時候生成快照,事務B在T2的時候插入一條數據Luffy,而後提交。在T4的時候把Luffy更新成Katakuri,根據上一個例子的經驗,此時事務A是看不到Luffy這條數據的,因此更新也不會成功,而且在T5的時候查詢,和T1時候同樣,只有Carson和Kata兩條數據。
可是,又要讓你失望了
執行結果以下
可是執行結果卻不是預期的那樣,事務A不只看到了Luffy,還把它成功的改爲了Katakuri。即便事務A成功commit以後,再次查詢仍是這樣。
這實際上是MySQL對insert
、update
和delete
語句所使用的當前讀(current read)。由於涉及到數據的修改,因此MySQL必須拿到最新的數據才能修改,因此涉及到數據的修改確定不能使用快照讀(snapshot read)。因爲事務A讀到了事務B已提交的新增數據,因此就產生了前文所說的幻讀。
那麼在Repeatable Read
隔離級別是怎麼解決幻讀的呢?
是經過間隙鎖
(Gap Lock)來解決的。咱們都知道InnoDB支持行鎖,而且行鎖是鎖住索引。而間隙鎖用來鎖定索引記錄間隙,確保索引記錄的間隙不變。間隙鎖是針對事務隔離級別爲Repeatable Read
或以上級別而設的,間隙鎖和行鎖一塊兒組成了Next-Key Lock
。當InnoDB掃描索引記錄的時候,會首先對索引記錄加上行鎖,再對索引記錄兩邊的間隙加上間隙鎖(Gap Lock)。加上間隙鎖以後,<font color="red">其餘事務就不能在這個間隙插入記錄。這樣就有效的防止了幻讀的發生</font>。
默認狀況下,InnoDB工做在Repeatable Read
的隔離級別下,而且以Next-Key Lock
的方式對索引行進行加鎖。當查詢的索引具備惟一性(主鍵、惟一索引)時,Innodb存儲引擎會對Next-Key Lock
進行優化,將其降爲行鎖,僅僅鎖住索引自己,而不是範圍(除非鎖定不存在的值)。如果普通索引,則會使用Next-Key Lock
將記錄和間隙一塊兒鎖定。</font>
使用快照讀的查詢語句
SELECT * FROM ...
使用當前讀的語句
SELECT * FROM ... lock in share mode SELECT * FROM ... for update INSERT INTO table ... UPDATE table SET ... DELETE table WHERE ...
本文主要講解了MySQL事務的ACID
四大特性,undo log
和redo log
分別實現了原子性和持久性,log持久化的三種方式,數據庫併發下的五類問題、四種隔離級別、RR隔離級別下select幻讀經過MVCC機制解決、select ... lock in share mode
/select ... for update
/insert
/update
/delete
的幻讀經過間隙鎖來解決。
本文涉及的比較深刻,掌握好本文的知識點,讓你不只僅是停留在ACID、隔離級別的層面,在面試中可以化被動爲主動,收割大廠offer。