本文主要探討MySQL的ACID是如何實現的,對于什么是事務,隔離個別以及鎖相關的只是不做過多的
討論。
MySQL作為當下最受歡迎的關系型數據庫,可應用于JAVA,Python,C++等諸多平臺,了解其底層實現,對于每一個軟件開發者是必不可少的過程。接下來就以Innodb為例,介紹一下MySQL的ACID在底層是如何實現的。
- Atomicity(原子性):一系列的SQL語句組成的邏輯單元,要么全部執行,要么全部都不執行。
- Consistency(一致性):事務執行前后,數據保持一致性。(如果把數據類比為能量,那么數據也遵循能量守恒定理)
- Isolation(隔離性):多個事務并發執行時,多個事務之間是相互隔離,不可見的。
- Durability(持久性):事務一旦提交,將會永久持久化到磁盤上,并不會由于操作系統宕機,MySQL服務器下線,硬件問題,或者斷電導致數據丟失。
隔離性
先簡單說說隔離性中的四種隔離級別:Read UnCommited<Read Commited<Repetable Read<Serialiable(可見性,描述的是不同事務之間能否獲取其他事務對數據庫數據所做的修改)
隔離級別 |
說明 |
Read UnCommited |
讀未提交,一個事務的修改,不管是否已將提交,對于其他事務都是可見的 |
Read Commited |
讀已提交,一個事務的修改,在提交之前,對于其他事務都是不可見的 |
Repetable Read |
可重復讀,一個事務中對相同查詢條件數據的多次讀取,結果都是一樣的 |
Serializable |
串行化,同步執行,效率太低,意義不大,一般不推薦使用 |
接下來了解一下,不同隔離級別產生的不同問題:臟讀,不可重復讀,幻讀
隔離級別 |
臟讀 |
不可重復讀 |
幻讀 |
讀未提交 |
√ |
√ |
√ |
讀已提交 |
× |
√ |
√ |
可重復讀 |
× |
× |
√ |
串行化 |
× |
× |
× |
那么多種隔離級別,隔離性是如何實現的,為什么不同事務之間能夠做到互不干擾?接下來就來了解一下隔離性的實現方案:鎖與MVCC
鎖
先來說說鎖的種類,看看MySQL中有哪些鎖
粒度
從粒度來區分鎖有表鎖,行鎖,頁鎖。表鎖又分為:共享鎖,排他鎖,自增鎖等。行鎖是在引擎層由各個引擎自己實現的,但是并不是所有的存儲引擎都包含行鎖,MyISAM就沒有行鎖。
行鎖的種類
在Innodb中,行鎖是通過給索引中的索引項加鎖來實現的,也就是只有通過使用索引來檢索數據才會使用到行鎖,否則使用的還是表鎖。行鎖同樣分為兩種:共享鎖和排他鎖,以及獲取鎖之前必須獲取當前表的意向共享鎖和意向排他鎖(先獲取表的意向共享鎖和意向排他鎖或者更強的鎖,然后才能給表中的每行記錄添加共享鎖和排他鎖)。
1、共享鎖:讀鎖,允許事務添加S鎖,不允許事務添加X鎖,即允許事務讀數據,不允許事務寫數據。
加鎖方式:select locak in share mode
2、排他鎖:寫鎖,不允許其他事務添加S鎖或者X鎖。加鎖方式:insert,delete,update,select for update
行鎖是在需要的時候才加鎖的,但并不是在不需要了就立刻釋放,而是要等到事務結束時才釋放。這個就是鎖的兩階段提交。
鎖之隔離性
大致介紹了一下鎖,我們可以知道,有了鎖,當事務正在寫數據時,其事務獲取不到寫鎖就無法寫數據,一定程度上保證了事務的隔離性。但是在數據庫的使用中,我們發現在寫數據的同時,我們還能讀數據,這就說明了MySQL中并不是單單依靠鎖來實現事務的隔離性。接下來我們就倆了解一種實現隔離性的一種比較折中的方式-----MVCC
MVCC
前面說到,有了鎖,事務就不能寫數據,但是還可讀數據,而且當其他事務已經對當前行進行了修改并提交,還是可以讀到重復數據。這就是多版本的并發控制,MVCC(Multi-Version Cocurrenty Controller)
版本連
innodb中的行記錄存儲格式中還有一些額外的字段:RowId,Data_trx_id和Data_roll_ptr
Data_trx_id:最后一次修改該行記錄的事務的事務ID
Data_roll_ptr:回滾指針。指向該行記錄的前一個版本。在undo log中以鏈表的形式組織
行記錄的信息圖
ReadView(讀視圖)
在每次查詢數據之前都會創建一個ReadView,具有以下一個屬性:
trx_ids:當前系統中活躍的事務ID的集合
low_trx_id:最晚創建的事務,當前事務鏈中最大的事務編號ID,也就是最近創建的,除本身之外最大的事務編號ID
up_trx_id:最先開始的事務,當前事務連中最小的事務編號ID,也就是當前系統中創建最早但還未提交的事務
creator_trx_ic:當前創建ReadView的事務ID編號
up_trx_id <= low_trx_id <= creator_trx_id
ReadView數據模型
開始查詢
一條SELECT語句過來,找到一行數據:
/*注意:最后修改包括:update | delete | insert*/
1、Data_tx_id < up_trx_id:說明最后修改該行記錄的事務之前就存在,并已提交,對其他事務可見的
2、Data_trx_id > low_trx_id : 說明最后修改改行記錄的事務還沒開始,對于其他事務不可見
up_trx_id < Data_trx_id < low_trx_id:查看trx_ids是否存在Data_trx_id,如果存在,說明最后修改改行記錄的
3、事務還沒提交,其他事務不可見,如果不存在,說明該條事務已經提交,其他事務可見
版本鏈示意圖
RR級別的幻讀
RR級別如何在RC級別上解決幻讀的:
事務A |
事務B |
|
事務A |
事務B |
開始事務 |
開始事務 |
|
開始事務 |
開始事務 |
快照讀查詢金額為500 |
快照讀查詢金額為500 |
|
快照讀查詢金額為500 |
|
更新金額為400 |
|
|
更新金額為400 |
|
提交事務 |
|
|
提交事務 |
|
|
select快照讀金額為500 |
|
|
select快照讀當前金額為400 |
|
select lock in share mode當前讀金額為400 |
|
|
select lock in share mode當前讀金額為400 |
1、左表與右標的唯一區別在于左表中事務B在事務A提交前做了一次快照讀,而右在事務A修改金額提交事務之前沒有進行一次快照讀。所以我們知道事務快照讀的結果非常依賴第一次事務快照讀的結果,既事務首次出現快照讀的結果非常關鍵,它決定該事務后續快照讀的結果
2、當一個事務中同時出現快照讀和當前讀時才會出現幻讀
3、MVCC并沒有徹底解決幻讀
原子性
在隔離性中MVCC依靠undo log中的版本鏈查找舊版本數據。在原子性的實現中同樣依賴于undo log。當事務對數據進行修改時innodb生成相應的undo log;如果事務執行失敗或者顯示調用rollback,便可以利用undo log將數據恢復到舊版本時的數據。undo log屬于邏輯日志,它記錄了SQL執行相關的信息。
- 對于每個insert,回滾會執行delete
- 對于每個delete,回滾時會執行insert
- 對于每個update,回滾時會執行一個相反的update將數據改回去
以update為例:當執行update時,undo log中會包含被修改行的主鍵(以便知道修改了哪些行),哪些列,以及相關的值,回滾時便可根據這些信息修改成update之前的狀態。
持久性
MySQL中有很多的日志文件,持久性依賴于redo log。
一條SQL更新語句怎么運行
持久性跟寫有關,MySQL中提供了WAL即使。WAL技術全稱Write-ahead logging,他的關鍵點在刷新磁盤之前,必須要先寫redo log.
Redo log
更新一行記錄的時候,Innodb會先將這條記錄寫到redo log(并更新內存),這個時候更新就算完成了。在適當的時候將這個操作記錄更新到磁盤中,這個往往是在比較空閑的時候進行,redo log有兩個特點:
1、大小固定,循環寫
2、crash-safe
Redo log有兩個狀態,稱為兩階段提交,如果不適用兩階段提交,會導致在數據庫狀態和用他的日志恢復處理的狀態不一致。
Buffer Pool
Innodb中還提供額緩存,Buffer pool保存了磁盤中部分數據頁的映射,作為訪問數據庫的緩沖。
讀取數據時,會向讀取buffer pool中的數據,如果沒有才會讀取磁盤中的數據
向數據庫寫入數據時,會先將數據寫入buffer pool,然后定期將buffer pool中寫入的書記刷新到磁盤
Buffer pool大大提高了數據可的讀寫效率,但也帶來了問題,當Buffer pool中的數據還沒有刷新到磁盤時,數據庫宕機,那么Buffer pool中的數據就丟失了,事務的持久性無法保證。所以還需借助redo log的輔助,更新數據時除了將數據更新到Buffer pool中,還會將此次更新操作計入到redo log當中。當提交事務時,會調用fsync將redo log刷新到磁盤中。
如果MySQL宕機時,重啟服務器時會讀取redo log中的數據,恢復數據庫中的數據。redo log采用WAL技術,所有的修改都會先寫入redo log中,再更新到buffer pool中,保證MySQL不會因為宕機導致MySQL丟失了持久性。而且這樣做有兩個優點:
1、刷臟也時隨機IO,redo log是順序IO
2、刷臟頁頁頁為單位,一條數據的修改整頁都要修改,redo log只包含真正需要修改的數據,減少了無效IO
bin log與redo log的區別
1、層次:redo log是Innodb存儲引擎特有的,而binlog是server中的,叫做歸檔日志文件
2、內容:redo log是物理文件,記錄某個數據頁上做了什么修改;而bin log是物理文件,
是語句的原始邏輯,如:給ID=2的這行記錄的c字段加2
3、寫入:redo log是循環寫,寫入世紀比較多,bin log是追加,再事務提交的時候寫入
一致性
一致性是事務追求的最終目標,有前面的原子性,持久性,隔離性保證。當然一致性除了數據庫的保障還要有業務層的保障,比如購買商品,除了扣除用戶余額之外,還要減少庫存,這樣才能保證數據一致性。
總結
MySQL 都很熟, ACID 也知道是個啥,但 MySQL 的 ACID 怎么實現的?
有時候,就像你知道了有 undo log、redo log 但可能并不太清楚為什么有,當知道了設計的目的,了解起來就會更加清晰了。