1. 事務
事務是指一個或者多個數據庫操作,要么全部沒有執行,要么全部成功執行。
中途失敗需要回滾到指定狀態,全部執行成功需要確保持久保存在數據庫中。
事務擁有四個特性,習慣上被稱之為ACID特性。
2. ACID特性
為了更直觀的解釋ACID特性,下面先說明A, B, C之間互相轉賬的過程。
假設A有10元,B有15元,C有8元
A給B轉賬5元,操作記為T1。
T1: read(A), A=A-5, write(A), read(B), B=B+5, write(B)。
T1操作的大體流程,先讀取A到賬戶余額,將A的賬戶余額扣減5元后再寫入數據庫中,
讀取B的賬戶余額,將B的賬戶余額增加5元后再寫入到數據庫中。
同時,C給B轉賬4元,操作記為T2。
T2: read(C), C=C-4, write(C), read(B), B=B+4, write(B)
T1操作的大體流程,先讀取C的賬戶余額,將C的賬戶余額扣減4元后再寫入數據庫中,
讀取B的賬戶余額,將B的賬戶余額增加4元后再寫入到數據庫中。
2.1 原子性(Atomicity)
事務作為一個整體被執行,包含在其中的對數據庫的操作要么全部被執行,要么都不執行,是一個最小執行單元,不可分割。
A給B轉賬5元的操作T1是包含多個讀寫操作,這些操作要么全部執行,要么全部不執行。
假設由于斷電等意外事件,導致T1只執行了部分操作,如T1:read(A), A=A-5, write(A)
這就會導致A憑空少了5元,并且B沒有收到A轉的5元,
因此事務需要保證保證在事務執行過程中出現錯誤時,將已經執行的操作“撤銷”,恢復到原始狀態。
2.2 一致性(Consistency)
事務應確保數據庫的狀態從一個一致狀態轉變為另一個一致狀態。
假設A有10元,B有15元,C有8元,不管A, B, C之間如何進行轉賬(沒有其他人參與),三個點賬戶總余額一定是33元,而不會是其它值。
2.3 隔離性(Isolation)
多個事務并發執行時,一個事務的執行不應影響其他事務的執行。
A給B轉賬5元,同時C給B轉賬4元,這個兩個事務應該是互相隔離的,互不影響。
最終A余額為5元,C余額為4元,B總共收到兩次轉賬,余額應該為24元。
假設T1, T2可以交叉執行,如下圖所示。最終結果看起來B只收到了A的5元轉賬,余額為20元。
2.4 持久性(Durability)
已被提交的事務對數據庫的修改應該永久保存在數據庫中。
MySQL操作一般是先寫入緩存,滿足指定條件后才將緩存更新到磁盤
磁盤寫操作相當耗時,而且同一個事務可能修改多個數據頁面,而且可能執行頁面中一個字節的數據。
因此每次數據庫提交執行緩存刷新磁盤操作不太合理,
MySQL設計人員通過redo日志來持久化最小量的數據來達到相同的效果。
3. redo日志
為了保證事務的持久性,在事務提交動作完成之前,需要把該事務修改所有頁面都刷新到磁盤,但是存在以下問題
隨機I/O比較慢,一個事務可能修改到多個頁面,這些頁面在磁盤中可能不相鄰,可能需要多次長距離移動磁盤讀寫磁頭。
刷新完整的數據頁面較慢,一個事務也可能值修改頁面中一個字節,卻要同步整個頁面到磁盤上。
為了解決上述到問題,InnoDB設計了redo日志,把事務修改的內容采用特定的格式按順序保存到磁盤上,
即使在系統崩潰之后,按照redo日志重新更改數據頁進行數據恢復即可。
3.1 redo日志格式
redo日志通用格式如下圖所示
type,redo日志類型,通過定義不同類型可以達到節省空間的目的
space id,表空間id
page number,頁號
data,redo日志具體內容
往表中插入一條記錄,可能產生多條redo日志,因為可能產生聚簇索引對應B+樹頁面的分裂操作,
可能需要性申請數據頁,金額可能要修改各種段、區的統計信息等,
最終插入一條記錄可能產生多條redo日志,這些日志是不可分割的,
在崩潰恢復時,也是將這一組日志作為不可分割的整體來處理,
類似的,將一組不可分割的redo日志稱為Mini-Transaction,即MTR
3.2 redo日志緩沖區
為了避免頻繁的磁盤IO,并不是每生成一條redo日志就同步到磁盤上。
而是先將redo日志放到緩沖區,在特定時機刷新到磁盤。
redo日志緩沖區頁面結構如下圖所示。
redo日志是以MTR為單位寫入到redo日志緩沖區的,redo日志緩沖區是有若干個512B大小的block構成的一片連續的內存空間,
InnoDB引擎使用lsn(log sequence number)來記錄系統當前有多少redo日志寫入到緩沖區
3.3 redo日志文件
3.3.1 flush鏈表中的lsn
InnoDB會將lsn相關信息寫入到flush鏈表中,進而方便判斷哪些redo日志文件可以被重復使用,
因為只要臟頁被刷新到磁盤,相應的redo日志內容就沒有存在的意義了,而且redo日志文件大小也有限。
Buffer Pool中頁面會在控制塊中記錄頁面的修改信息
oldest_modification: 第一次修改Buffer Pool中某個頁面時,將MTR開始時的lsn寫入該變量
newest_modification: 每次修改Buffer Pool中某個頁面時,將MTR結束時的lsn寫入該變量
flush鏈表與oldest_modification(o_m)和newest_modification(n_m)的關系如下圖所示。
flush鏈表的基節點start指針出發,flush鏈表的臟頁時按照第一次修改發生的時間倒序排列的,也就是按照oldest_modification代表的lsn值倒敘排列,
當頁面被多次更新時,會更新對應頁面的newest_modification變量的值。
當頁面1被屬性到磁盤,從頁面2的控制塊可以看出,lsn低于8916的redo日志可以被覆蓋,系統會將8916賦值到redo文件的checkpoint_lsn的操作。
InnoDB將檢查flush鏈表最小的oldest_modification的lsn值稱為checkpoint操作。
3.3.2 redo日志文件格式
磁盤上存在多個redo日志文件,會被循環使用,這一組redo日志文件稱為redo日志文件組。
和redo日志緩沖區一樣,redo日志文件也是由若干個512B構成的block組成
其中,redo日志文件的頭2048個字節用于存儲一些管理信息,系統會將checkpoint操作得到的checkpoint_lsn賦值到checkpoint1的checkpoint_lsn上。
崩潰恢復會從checkpoint_lsn在日志文件組中對應的偏移量開始。
除了前面闡述的checkpoint,redo日志刷盤時機還包括
redo日志緩存不足時
事務提交時
后臺線程周期性刷盤
正常關閉服務器
3.3.3 奔潰恢復
當遇到異常情況導致服務器掛掉,在重啟時可以根據redo日志文件恢復到奔潰前的狀態。
InnoDB從redo日志文件組的第一個文件的checkpoint信息,然后從checkpoint_lsn在日志文件組中對應的偏移量開始,
一直掃描日志文件中的,直到某個block的寫入量的值不等于512,根據redo日志格式將修改的內容恢復到奔潰前狀態。
4. undo日志
在事務執行過程中可能遇到各種錯誤,導致中途就結束事務了,但是在遇到錯誤退出前,可能修改多個行記錄,
但是為了保證事務的原子性,需要將數據恢復到事務開啟前,這個恢復過程就稱為回滾。
為了回滾,就需要將事務修改的內容記錄下來,包括插入的行記錄、修改行記錄的內容、刪除的行記錄,
保存事務執行過程中修改內容的東西稱為undo日志。
4.1 undo日志格式
4.1.1 聚簇索引行結構
InnoDB會將聚簇索引行結構如下圖所示
InnoDB會將聚簇索引行結構補充trx_id和roll_pointer兩個隱藏列
trx_id: 一個事務某次對某條聚簇索引記錄進行改動時,都會把該事務的事務ID賦值給trx_id,事務ID是單調遞增的
roll_pointer: 每次對某條聚簇索引進行改動時,都會把舊版本寫入到undo日志中,并以聚簇索引為起點構成一個從最新到最舊的單向鏈表結構,這個鏈表就稱為版本鏈
roll_pointer結構如下圖所示
is_insert, 表示該指針指向的undo日志是否是TRX_UNDO_INSERT大類的undo日志
resg id, 表示該指針指向的undo日志的回滾段編號
page number, 表示該指針指向的undo日志所在頁面的頁號
offset, 表示該指針指向的undo日志在頁面中的偏移量
4.1.2 插入操作
如果需要回滾插入操作,只需要將插入的記錄刪除即可,
因此在記錄undo日志時,只需要記錄插入的記錄的主鍵信息即可,通過主鍵能找到唯一的記錄
插入操作的對應的undo日志類型為TRX_UNDO_INSERT_REC,結構如下圖所示
undo type, 即TRX_UNDO_INSERT_REC
undo no, 事務執行過程中,每生成一條undo日志,undo no就增加1,且從0開始
table id, 該undo日志對應的記錄所在表的table id
4.1.3 刪除操作
在事務中執行刪除操作,會將記錄的deleted_flag標識為值為1,但該記錄依然在正常記錄鏈表,并沒有移動到垃圾記錄鏈表,這個過程稱為delete mark。
在事務提交后,才把該記錄從正常記錄鏈表挪到垃圾記錄鏈表
刪除操作產生TRX_UNDO_DEL_MARK_REC類型的日志,結構如下圖所示,
在對一條記錄進行delete mark操作前,將該記錄的trx_id和roll_pointer的舊值保存到undo日志的trx_id和roll_pointer變量中。
假設一個事務對某條記錄先更新再刪除,這樣就能通過TRX_UNDO_DEL_MARK_REC找到更新的undo日志。
4.2.4 更新操作
更新操作的場景較復雜,InnoDB將其分為更新主鍵和不更新主鍵兩種場景。
不更新主鍵
更新的每個列在更新前后占用的存儲空間不變,則可以進行就地更新
更新前后占用存儲空間有變,需要將舊記錄從聚簇索引刪除,再創建一條新的記錄
更新主鍵,舊記錄執行delete mark操作,創建新紀錄插入聚簇索引
針對以上各種情況,InnnoDB設計了對應的undo日志格式,限于篇幅這里就不展開說明。
4.2 undo日志頁面
和InnoDB普通頁面結構類型,undo日志頁面結構及頁面鏈表如下圖所示。
一個undo日志頁面只能存儲一種類型,InnoDB將undo日志分為兩大類,
TRX_UNDO_INSERT,由insert語句產生undo日志,或者update語句更新主鍵也會產生該類型的undo日志
TRX_UNDO_UPDATE,除了TRX_UNDO_INSERT類型的undo日志,其它類型的undo日志都屬于這個大類
InnoDB對臨時表和普通表產生的undo日志分開記錄,因此一個事務最多可能需要4個undo頁面鏈表。
4.3 回滾
同一個時刻,可能存在多個事務在執行,為了更好的管理undo頁面鏈表,
InnoDB設計了Rollback Segment Header頁面用于存放各個Undo頁面鏈表的第一個undo頁面的頁號,即undo slot。
在奔潰恢復時,需要將未提交事務的修改回滾掉,通過undo slot找到undo頁面鏈表,
通過判斷undo頁面鏈表的Undo Log SegmentHeader的TRX_UNDO_STATE屬性值,
如果為TRX_UNDO_ACTIVE,則進一步通過undo鏈表最后一個頁面的Undo Log Header中找到該事務對應的事務ID,
然后通過undo日志內容將該事務修改的內容全部撤銷,從而保證事務的原子性。
5. 事務隔離級別和MVCC
5.1 常見一致性問題
臟寫(Dirty Write)
一個事務修改了另外一個未提交事務修改過的數據。
臟讀(Dirty Read)
一個事務讀取了另外一個未提交事務修改過的數據。
不可重復讀(Non-repeatable Read)
一個事務修改了另一個未提交事務讀取的數據。
幻讀(Phantom)
一個事務根據某些搜索條件查詢出一些記錄,在該事務未提交時,另一個事務寫入了一些符合搜索條件的記錄,
再次以相同條件查詢,前后兩次結果不一致。
5.2 事務隔離級別
SQL標準中定義了四種隔離級別
讀未提交(Read Uncommitted), 讀已提交(Read Committed), 可重復讀(Repeatable Read), 串行化(Serializable)。
不同隔離級別對應的可能和不可能發生的一致性問題如下圖所示。
其中臟寫問題是不允許發生的。
5.3 MVCC
5.3.1 版本鏈
InnnoDB存儲引擎的聚簇索引的版本鏈如下圖所示
假設在某個時刻,事務321、315、301對某條記錄進行Updata操作后,形成的版本鏈如下圖所示。其中事務301對這條記錄更新了兩次
5.3.2 MVCC和ReadView
Multi-Version Concurrency Control, 即多版本并發控制,
多版本并發控制機制利用聚簇索引的版本鏈來控制并發事務訪問相同記錄時的行為,從而解決臟讀和不可重復讀的不一致性問題。
ReadView,即一致性視圖,通過這個視圖可以判斷版本鏈的某個版本是否可被當前事務訪問,中ReadView包含以下4個比較重要的數據
creator_trx_id,生成該ReadView的事務對應的事務ID
m_ids,在生成ReadView時當前系統中活躍的事務ID列表
min_trx_id,m_ids的最小值
max_trx_id,生成ReadView時,系統的下一個事務ID值。
判斷是否可見的規則
如果被訪問版本的trx_id值與ReadView中的creator_trx_id值相同,說明是當前事務在訪問自己修改過的記錄,該版本可以被當前事務訪問
如果被訪問版本的trx_id值小于ReadView中min_trx_id值,說明該版本在當前事務生成ReadView前已經提交,因此該版本可以被訪問
如果被訪問版本的trx_id值不小于ReadView中的max_trx_id值,說明該版本的事務在當前事務生成ReadView后啟動,因此該版本不可訪問
如果被訪問版本的trx_id值在ReadView的min_trx_id和max_trx_id之間
如果trx_id在m_ids列表中,說明ReadView生成時該版本的事務依然活躍,因此該版本不可訪問
如果trx_id不在在m_ids列表中,說明ReadView生成時該版本的事務已經提交,因此該版本可以被訪問
順著版本鏈重復上述操作,直到找到可以訪問的版本,或者到達版本鏈末尾。如果版本鏈最后一個版本依然不可見,則查詢結果為記錄不存在
在讀已提交和可重復讀隔離級別下,ReadView生成的時機有所不同,
讀已提交在每一次進行普通Select操作前都會生成一個ReadView,確保了讀取到都是已提交事務的數據
可重復讀只在第一次進行普通Select操作前生產一個ReadView, 之后查詢操作都重復使用這個ReadView,保證了同一個事務內不同時間讀到相同數據
-
SQL
+關注
關注
1文章
760瀏覽量
44077 -
磁盤
+關注
關注
1文章
367瀏覽量
25177 -
數據庫
+關注
關注
7文章
3765瀏覽量
64276
原文標題:一文讀懂SQL事物機制
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論