MySQL(六):MySQL之MVCC

无虑的小猪發表於2024-03-08

1、事務的引入

  事務是資料庫管理系統(DBMS)執行過程中的一個邏輯單位(不可再進行分割),由一個有限的資料庫操作序列構成(多個DML語句),要不全部成功,要不全部不成功。

  如:A 給 B 劃錢,A 的賬戶-100元, B 的賬戶就要+100元,這兩個update 語句必須作為一個整體來執行,不然A 扣錢了,B 沒有加錢這種情況就是錯誤的。那麼事務就可以保證A 、B 賬戶的變動要麼全部一起發生,要麼全部一起不發生。

2、事務的特性

  事務具有ACID特性:原子性、一致性、隔離性、永續性。

2.1、原子性(atomicity)

  一個事務必須被視為一個不可分割的最小單元,整個事務中所有的操作要麼全提交成功,要麼全部失敗。

  A 給 B 轉賬 100,A 的賬戶 -100,B 的賬戶 +100,整個事務的操作要麼全部成功,要麼全部失敗,不能出現 A的賬戶金額扣除,B的賬戶金額不扣除,原子性不能得到保證,會出現一致性問題。

2.2、一致性(consistency)

  一致性是指事務將資料庫從一種一致性轉換到另外一種一致性狀態,在事務開始之前和事務結束之後資料庫中資料的完整性沒有被破壞。

  A 給 B 轉賬 100,A 的賬戶 -100,B 的賬戶 +100。A賬戶扣除的錢(-100),B賬戶增加的錢(+100)相加應該為0,兩個賬戶的錢加起來前後應該不變。

2.3、隔離性(isolation)

2.3.1、概念

  一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的資料對併發的其他事務是隔離的,併發執行的各個事務之間不能互相干擾。

2.3.2、隔離性的導致的問題

  若不能保證隔離性,會導致何種問題?

  A賬戶的原始金額1000,轉賬兩次,每次都是100,B賬戶的原始金額500,從理論上,轉賬完成後,A賬戶的金額有800,B賬戶的金額有700。

  將兩次轉賬操作分別稱為T1和T2,T1和T2的操作可能交替執行,執行順序可能是:

0

  T1和T2交替執行,若按照上述執行順序來進行兩次轉賬,最終的結果:A賬戶餘額900,B賬戶餘額700。A賬戶相當於只扣了100元,B賬戶卻多了200元。

  這種狀態轉換對於的某些資料庫操作來說,不僅要保證這些操作以原子性的方式執行完成,而且要保證其它的狀態轉換不會影響到本次狀態轉換,這個規則稱之為隔離性。

2.3.3、事務併發引發的問題

  MySQL是一個客戶端/伺服器架構的軟體,對於同一個伺服器來說,可以有若干個客戶端與之連線,每個客戶端與伺服器連線上之後,可以稱之為一個會話(Session)。每個客戶端都可以在自己的會話中向伺服器發出請求語句,一個請求語句可能是某個事務的一部分,也就是對於伺服器來說可能同時處理多個事務。

  事務由隔離性的特性,理論上在某個事務對某個資料進行訪問時,其他事務應該排隊,當該事務提交之後,其他事務才可以繼續訪問這個資料,如此併發事務的執行就變成了序列化執行。

  序列化執行效能影響太大,當既想保持事務一定的隔離性,又想保證伺服器在處理訪問同一資料多個事務的效能。當捨棄隔離性時,會帶來什麼樣的資料問題?

3.1、髒讀

  當一個事務讀物到另一個事務修改但未提交的資料,被稱為髒讀。

0

  在事務A執行過程中,事務A對資料進行了修改,事務B讀取了事務A修改後的資料;

  由於某些原因,事務A並未完成提交,發生了RollBack操作,則事務B讀取的資料就是髒資料。

  這種讀取到另一個事務未提交的資料的現象就是髒讀(Dirty Read)。

3.2、不可重複讀

  當事務內相同的記錄被檢索兩次,且兩次得到的結果不同時,此現象稱為不可重複讀。

0

  事務B讀取了兩次是資料資源,在這兩次讀取的過程中事務A修改了資料,導致事務B在這兩次讀取出來的資料不一致。

3.3、幻讀

  在事務執行過程中,另一個事務將新紀錄新增到正在讀取的事務中,會發生幻讀。

0

  事務B前後兩次讀取同一個範圍的資料,在事務B兩次讀取的過程中事務A新增了資料,導致事務B後一次讀取到前一次查詢沒有看到的行。

  幻讀重點強調了讀取到了之前讀取沒有獲取到的記錄。

2.4、永續性(durability)

  事務一旦提交,則其所做的修改就會永久儲存到資料庫中。即使系統崩潰,已經提交的修改資料也不會丟失。

3、事務的隔離級別

  對於不同的隔離級別,併發事務可以發生不同嚴重程度的問題,具體情況如下:

隔離級別
含義
髒讀
不可重複讀
幻讀
READ UNCOMMITTED
讀未提交
READ COMMITTED
已提交讀
-
REPEATABLE READ
可重複讀
-
-
SERIALIZABLE
可序列化
-
-
-

  MySQL的預設隔離級別為 REPEATABLE READ。

3.1、讀未提交(read uncommitted)

  事務A在修改表中資料時,可以看到事務B未提交的資料。安全級別最低的隔離級別,會產生髒讀(dirty read)。

3.2、讀已提交(read committed)

  事務A和事務B同時操作同一張表,有一條資料是id為1姓名為CPP的資料,此時事務A對這個資料修改為id為2姓名為ZYC(未提交),事務B再次查詢得到的資料一直都是 1 CPP,當事務A修改完成commit提交後,此時事務B再次查詢得到的資料 2 ZYC。

0

  讀已提交,只能讀取已提交的資料,該隔離級別解決了髒讀的問題,但缺點是不可重複讀。

3.3、可重複讀(REPEATABLE READ)

  可重複讀是MySQL的預設隔離級別。

  0

  事務A和事務B同時操作同一張表,有一條資料是id為1姓名CPP的資料,此時事務A對這個資料修改為id為1姓名ZYC(未提交),此時事務B查詢id為1的記錄,姓名仍為CPP。事務A提交完成後,事務B再次查詢id為1的記錄姓名仍為CPP。

  在事務B未commit前,不管查詢多少次結果id為1的記錄,姓名都為CPP。只有在事務B提交後,再次查詢id為1的記錄,才會查出最新結果,姓名為ZYC。

  可重複讀的隔離級別解決了不可重複讀的問題,但會出現幻讀。如當查詢某條資料,可能顯式的是1 CPP,但此時可能有個人正好修改好了這條資料,當你提交後再次查詢時,會發現變成了2 ZYC。

3.4、序列化

  最安全的預設隔離級別,但不支援併發,相當於單執行緒,不允許併發運算元據庫,事務開啟後,只允許一個人進行操作,直到提交後下一個人才可以進入。

4、MVCC

  MVCC全稱Multi-Version Concurrency Control,即多版本併發控制,主要是為了提高資料庫的併發效能。

  每個不同的事務訪問查詢同一行資料時,每個事務修改的都是這行資料的不同版本,InnoDB只需要去記錄這個資料的訪問鏈,就可以實現一個SELECT操作的併發執行。

  同一行資料同時發生讀寫請求時,會上鎖阻塞住。但MVCC用更好的方式去處理讀--寫請求,做到在發生讀--寫請求衝突時不用加鎖。這個讀指的是快照讀,而不是當前讀,當前讀是一種加鎖操作,是悲觀鎖。

  如何做到讀--寫不用加鎖的,快照讀和當前讀指什麼?

  MVCC的實現主要依賴於記錄中的隱藏欄位,undolog,readview來實現的。

4.1、版本鏈

  使用InnoDB儲存引擎的表的聚簇索引記錄中都包含兩個必要的隱藏列:trx_id 和 roll_pointer。

  trx_id:每次一個事務對某條聚簇索引記錄進行改動時,會把該事務的事務id賦值給trx_id隱藏列;

  roll_pointer:每次對某條聚簇索引記錄進行改動時,會把舊的版本寫入到undo日誌中,然後這個隱藏列就相當於一個指標,可以透過它來找到該記錄修改前的資訊。

  0

  假設現有一個事務B,事務ID為10,要對這條記錄進行修改,把CPP的age從12改成了20,那麼此時Undo Log會發生啥?

  此時,這條id為1的記錄,trx_id就變為了10,trx_id此時記錄了修改這條記錄的事務ID,而對應的roll_pointer指標,就指向了上次事務A的操作對應的Undo Log:

  0

  trx_id是記錄修改了每條聚簇索引的事務id;roll_pointer是一個指標,指向每一個歷史操作版本的資料儲存的地址;每一次修改操作都會生成一個Undo Log版本,每個版本之間是隔離的。

  undo日誌:為了實現事務的原子性,InnoDB儲存引擎在實際進行增、刪、改一條記錄時,都需要先把對應的undo日誌記下來。一般每對一條記錄做一次改動,就對應著一條undo日誌,但在某些更新記錄的操作中,也可能會對應著2條undo日誌。一個事務在執行過程中可能新增、刪除、更新若干條記錄,也就是說需要記錄很多條對應的undo日誌,這些undo日誌會被從0開始編號,也就是說根據生成的順序分別被稱為第0號undo日誌、第1號undo日誌、...、第n號undo日誌等,該編號也稱為undo no。

  該記錄每次更新後,都會將舊值放到一條undo日誌中,是該記錄的一箇舊版本,隨著更新次數的增多,所有的版本都會被roll_pointer屬性連線成一個連結串列,將這個連結串列稱之為版本鏈,版本鏈的頭節點就是當前記錄最新的值,每個版本中還包含生成該版本時對應的事務id。可以利用這個記錄的版本鏈來控制併發事務訪問相同記錄的行為,這種機制被稱之為多版本併發控制(Mulit-Version Concurrency Control MVCC)。

  若有事務C,事務D等一直對這條記錄進行修改,那麼這條記錄的roll_pointer指標就會一直這樣遞迴修改下去,最終形成一個關於修改和刪除操作的Undo Log版本鏈。

  修改和刪除操作才會生成Undo Log版本鏈,查詢操作不會生成Undo Log版本鏈。

  InnoDB有兩種版本鏈:insert undo log(插入操作產生)和 update undo log(更新操作產生)。

  MySQL的讀操作和寫操作是分離的,寫操作去生成版本鏈,而讀操作只需要根據規則去檢視對應的某一個版本,即快照讀。

4.2、Read View

  Read View 是事務進行快照讀操作時產生的讀檢視,在該事務執行快照讀的那一刻,會生成一個資料系統當前的快照,記錄並維護系統當前活躍事務的id,事務的id是遞增的。

  Read View的最大作用是用來做可見性判斷的,即當某個事務在執行快照讀時,對該記錄建立一個Read View的檢視,把它當作條件去判斷當前事務能夠看到哪個版本的資料,有可能讀取到的是最新的資料,也有可能讀取的是當前行記錄的undolog中某個版本的資料。

  Read View 存放著一個列表,這個列表用來記錄當前資料庫系統中活躍的讀寫事務,也就是已經開啟了,正在進行資料操作但是還未提交儲存的事務。可以透過這個列表來判斷某一個版本是否對當前事務可見。其中,有四個重要的欄位:

creator_trx_id
建立當前Read View所對應的事務ID
m_ids
所有當前未提交事務的事務ID,即活躍事務的事務id列表
min_trx_id
m_ids裡最小的事務id值
max_trx_id
InnoDB 需要分配給下一個事務的事務ID值

  0

  如何判斷當前版本對每一個事務是否可見,就是拿它們的值進行區間比較。

  Read View遵循的可見性演算法主要是將要被修改的資料的最新記錄中的DB_TRX_ID(當前事務id)取出來,與系統當前其他活躍事務的id去對比,如果DB_TRX_ID跟Read View的屬性做了比較,不符合可見性,那麼就透過DB_ROLL_PTR回滾指標去取出undolog中的DB_TRX_ID做比較,即遍歷連結串列中的DB_TRX_ID,直到找到滿足條件的DB_TRX_ID,這個DB_TRX_ID所在的舊記錄就是當前事務能看到的最新老版本資料。

4.3、MVCC實現可重複讀

  假設現在事務A和事務B同時對主鍵id = 1進行操作。事務A的id為20,事務B的id為30。事務A將age更新成15,事務B將age更新成30。

  0

  事務A和事務B建立各自的Read View,此時事務A的creator_trx_id = 10,事務B的creator_trx_id = 20。

事務A
事務id為10
事務B
事務id為20
creator_trx_id = 10
creator_trx_id = 20
m_ids = [10,20]
m_ids = [10,20]
min_trx_id = 10
min_trx_id = 10
max_trx_id = 21
max_trx_id = 21

  當前有兩個活躍的事務,事務id分別為10和20。事務列表中最小的事務id是事務A,min_trx_id為10,max_trx_id值為事務B的下一個id,也就是21。

  事務A去讀取主鍵id為1的資料,找到了記錄後就會去檢視該記錄的trx_id,事務A檢視到該記錄的trx_id的值為5。

  0

  將事務A的creator_trx_id與當前記錄的trx_id的值進行比較:

  0

  id=1的資料記錄 trx_id=5 < 事務A的 creator_id=10,判斷該記錄到的事務id不存在於活躍的事務列表中並且小於事務A的事務id,表示本次記錄的值是在事務A查詢之前提交的,可以放心讀取。讀取完畢,會將該記錄的trx_id修改為自己的事務id。

  0

  事務A將cpp的age從10又改成了15。

  0

  同時另一個隱藏欄位也會被修改,roll_pointer指標。會指向被事務A修改之前的版本,也就是cpp的年齡還是15時候的地址,為了用來記錄,方便下次被查詢:

  0

  之後,事務B參與進來,事務B也對主鍵id為1的update操作,將cpp的age從15更改為30。此時再進行一次trx_id的比較過程,去判斷自己的creator_trx_id是否大於這條記錄對應的trx_id,若大於則去修改這條記錄的值,將age從30修改為50:

  0

  0

  若事務A再次去讀取主鍵id=1記錄的值,這條記錄的trx_id已經變成了20,會再次進行值的區間比較:發現事務A下資料記錄的 trx_id(10)<主鍵id=1資料記錄的 trx_id(20)<max_trx_id(21),並且trx_id為20的值存在於m_ids中,代表自己讀取到的是和事務a同一時間範圍內一塊啟動的另一個未提交的活躍事務b所修改的值。< div="">

  0

  此時事務A不會去讀取這條記錄對應的資料,會通知Undo Log上的roll_pointer指向的地址去查詢上一個1舊版本的記錄,直到找到第一條trx_id小於等於自己的事務id並且不存在於m_ids列表中的記錄,即為別的事務已經提交的最後一條記錄並讀取它。

  0

  每一個事務去讀取或者修改同一個記錄時,只能操作已經提交了的資料,未提交的資料是不能讀取到的。

  本質上就是透過Read View的欄位判斷這行記錄對自己是否可見,如果不可見的話再去找Undo Log裡面記錄的對自己可見的資料。

4.4、MVCC實現讀已提交

  提交讀能夠解決髒讀併發一致性問題。髒讀問題本質上是一個事務讀取到了另一個事務沒有提交的內容。下面來看看ReadView要如何解決該問題?

  事務A和事務B同一時刻開啟,事務B將同一行的記錄,將cpp的age改成了35,但並未提交,此時事務A讀取這條記錄。

  0

  0

  事務A檢視到該記錄的trx_id比事務A自身的Read View列表裡的creator_trx_id值大,並且修改這條記錄的事務的trx_id存在於m_ids列表中,事務A就可以判斷得到該記錄是被另一條沒有提交的事務修改的,所以事務A不會讀取這條資料的內容。

  0

  事務A會繼續透過Undo Log往下找第一條trx_id小於等於自己的事務id並且不再活躍事務列表m_ids裡面的資料。因此,不會看到別的事務正在修改的資料,髒資料也不會產生。

  0

  0

4.5、總結

  InnoDB儲存引擎中,MVCC透過 隱藏列 + Undo Log + Read View 進行資料讀取,Undo Log儲存了歷史快照,Read View規則用於判斷當前版本的資料是否可見,不需要透過加鎖的方式,就可以實現提交讀和可重複讀這兩種隔離級別。

4.5.1、RC、RR級別下的InnoDB快照讀的區別

  造成RC、RR級別下快照讀的結果的不同是因為Read View生成時機的不同。

  1、在RR級別下的某個事務的對某條記錄的第一次快照讀會建立一個快照即Read View,將當前系統活躍的其他事務記錄起來,此後在呼叫快照讀的時候,還是使用的是同一個Read View,所以只要當前事務在其他事務提交更新之前使用過快照讀,那麼之後的快照讀使用的都是同一個Read View,所以對之後的修改不可見

  2、在RR級別下,快照讀生成Read View時,Read View會記錄此時所有其他活動和事務的快照,這些事務的修改對於當前事務都是不可見的,而早於Read View建立的事務所做的修改均是可見

  3、在RC級別下,事務中,每次快照讀都會新生成一個快照和Read View,這就是我們在RC級別下的事務中可以看到別的事務提交的更新的原因。

  在RC隔離級別下,是每個快照讀都會生成並獲取最新的Read View,而在RR隔離級別下,則是同一個事務中的第一個快照讀才會建立Read View,之後的快照讀獲取的都是同一個Read View。

5、其他

5.1、隱式提交

  MySQL使用START TRANSACTION或者BEGIN語句開啟事務,系統變數autocommit的值設定為OFF時,事務不會進行自動提交,當輸入了某些語句(create/alter),就好像輸入了commit語句,這種因某些特殊的語句而導致事務提交的情況稱為隱式提交,這些會導致事務隱式提交的語句包括:ALTER、CREATE、DROP、GRANT等。

  定義或修改資料庫物件的資料定義語言(Datadefinition language,簡稱DDL)。資料庫物件指的是資料庫、表、檢視、儲存過程等。當使用CREATE、ALTER、DROP等語句去修改資料庫物件時,會隱式的提交前面語句所屬的事務。

BEGIN;
SELECT ... -- 事務中的一條語句
UPDATE ... -- 事務中的一條語句
... 事務中的其他語句
CREATE TABLE ...  -- 隱式提交事務

5.2、儲存點

  若開啟了一個事務,執行了很多語句,其中的某條語句有問題,此時需要使用ROLLBACK語句讓資料庫狀態恢復到事務執行之前的樣子,但可能根據業務和資料的變化,不需要全部回滾。

  MySQL裡提出了一個儲存點(savepoint)的概念,即在事務對應的資料庫語句中打幾個點,在呼叫ROLLBACK語句時可以指定回滾到哪個店,而不是回到最初的原點。定義儲存點的語法:

SAVEPOINT 儲存點名稱;

  若想回滾到某個儲存點時,可以使用如下的語句:

ROLLBACK TO [SAVEPOINT] 儲存點名稱;

  若ROLLBACK語句後邊不跟隨儲存點名稱,會直接回滾到事務執行之前的狀態。

  若要刪除某個儲存點,可以使用如下語句:

RELEASE SAVEPOINT 儲存點名稱;

相關文章