Spring事务专题(三)事务的基本概念,Mysql事务处理原理
我重新整理了大綱,思考了很久,決定單獨將MySQL的事務實現原理跟Spring中的事務示例分為兩篇文章,因為二者畢竟沒有什么實際關系,實際上如果你對MySQL的事務原理不感興趣也可以直接跳過本文,等待接下來兩篇應用及源碼分析,不過我覺得知識的學習應該慢慢行成一個體系,為了建立一個完善的體系應該要對數據庫本身事務的實現有一定認知才行。
本文為Spring事務專題第三篇,在前兩篇文章中我們已經對Spring中的數據訪問有了一定的了解,那么從本文開始我們正式接觸事務,在分析Spring中事務的實現之前我們應該要對事務本身有一定的了解,同時也要對數據庫層面的事務如何實現有一定了解。話不多說,我們開始正文
本文大綱:
MYSQL事務大綱?
初識事務
為什么需要事務?
這里又要掏出那個爛大街的銀行轉賬案例了,以A、B兩個賬戶的轉賬為例,假設現在要從A賬戶向B賬戶中轉入1000員,當進行轉賬時,需要先從銀行賬戶A中取出錢,然后再存入銀行賬戶B中,SQL樣本如下:
//?第一步:A賬戶余額減少減少1000?? update?balance?set?money?=?money?-1000?where?name=?‘A’; //?第二步:B賬戶余額增加1000?? update?balance?set?money?=?money?+1000?where?name=?‘B’;如果在完成了第1步的時候突然宕機了,A的錢減少了而B的錢沒有增加,那A豈不是白白丟了1000元,這時候就需要用到我們的事務了,開啟事務后SQL樣本如下:
//?第一步:開始事務 start?transaction; //?第二步:A賬戶余額減少減少1000?? update?balance?set?money?=?money?-1000?where?name=?‘A’; //?第三步:B賬戶余額增加1000?? update?balance?set?money?=?money?+1000?where?name=?‘B’; //?第四步:提交事務 commit;什么是事務
事務(Transaction)是訪問和更新數據庫的程序執行單元;事務中可能包含一個或多個sql語句,這些語句要么都執行成功,要么全部執行失敗。
事務的四大特性(ACID)
-
原子性(Atomicity,或稱不可分割性)
「一個事務必須被視為一個不可分割的最小工作單元,整個事務中所有的操作要么全部提交成功,要么全部失敗回滾,對于一個事務來說,不可能只執行其中的一部分操作,這就是事務的原子性」
-
一致性(Consistency)
「數據庫總是從一個一致性的狀態轉換到另外一個一致性的狀態,在事務開始之前和之后,數據庫的完整性約束沒有被破壞。在前面的例子中,事務結束前后A、B賬戶總額始終保持不變」
-
隔離性(Isolation)
「隔離性是指,事務內部的操作與其他事務是隔離的,并發執行的各個事務之間不能互相干擾。嚴格的隔離性,對應了事務隔離級別中的Serializable (可串行化),但實際應用中出于性能方面的考慮很少會使用可串行化。」
-
持久性(Durability)
「持久性是指事務一旦提交,它對數據庫的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響。」
事務的隔離級別
在前文中我們介紹了隔離性,但實際上隔離性比想象的要復雜的多。在SQL標準中定義了四種隔離級別,每一種隔離級別都規定了一個事務所做的修改,哪些在事務內和事務間是可見的,哪些是不可見的,較低級別的隔離通常可以執行跟高的并發,系統的開銷也更低
未提交讀(READ UNCOMMITTED)
在這個隔離級別下,事務的修改即使沒有提交,對其他事務也是可見的。事務可以讀取未提交的數據,這也被稱之為臟讀。這個級別會帶來很多問題,從性能上來說,READ UNCOMMITTED不會比其他的級別好太多,但是卻會帶來很多問題,除非真的有非常必要的理由,在實際應用中一般很少使用。
提交讀(READ COMMITED)
大多數數據系統的默認隔離級別都是READ COMMITED(MySql不是),READ COMMITED滿足前面提到的隔離性的簡單定義:一個事務開始時,只能看到已經提交的事務所做的修改。換句話說,一個事物從開始直到提交前,所做的修改對其他事務不可見。這個級別有時候也叫做不可重復讀,因為執行兩次相同的查詢可能會得到不同的結果。
可重復讀(REPEATABLE READ)
REPEATABLE READ解決了臟讀以及不可重復度的問題。該級別保證了同一個事務多次讀取同樣記錄的結果是一致的。但是理論上,可重復讀還是無法解決另外一個幻讀的問題。所謂幻讀,指的是當某個事務在讀取某個范圍內的記錄時,另外一個事務又在該范圍內插入了新的記錄,當之前的事務再次讀取該范圍的記錄時,就會產生幻讀。
不可重復讀跟幻讀的區別在于,「前者是數據發生了變化,后者是數據的行數發生了變化」。
可串行化(SERIALIZABLE)
SERIALIZABLE是最高的隔離級別,它通過強制事務串行執行,避免前面說的幻讀。簡單來說SERIALIZABLE會在讀取的每一行數據上都加鎖,所以可能會導致大量的超時和鎖爭用的問題。實際應用中也很少使用這個隔離級別,只有在非常需要確保數據一致性而且可以接受沒有并發的情況下,才考慮此級別。
保存點
我們可以在事務執行的過程中定義保存點,在回滾時直接指定回滾到指定的保存點而不是事務開始之初,有點像我們玩游戲的時候可以存檔而不是每次都要重新再來。
定義保存點的語法如下:
SAVEPOINT?保存點名稱;當我們想回滾到某個保存點時,可以使用下邊這個語句(下邊語句中的單詞WORK和SAVEPOINT是可有可無的):
ROLLBACK?[WORK]?TO?[SAVEPOINT]?保存點名稱;MySQL中的事務跟原理
MySQL中的事務
「MySQL中不是所有的存儲引擎都支持事務」,例如MyISAM就不支持事務,實際上支持事務的只有InnoDB跟NDB Cluster,「本文關于事務的分析都是基于InnoDB」
「MySQL默認采用的是自動提交的方式」,也就是說如果不是顯示的開始一個事務,則系統會自動向數據庫提交結果。在當前連接中,還可以通過設置AUTOCONNIT變量來啟用或者禁用自動提交模式。
-
開啟自動提交功能
MySQL中默認情況下的自動提交功能是已經開啟的。
-
關閉自動提交功能。
關閉自動提交功能后,只用當執行COMMIT命令后,MySQL才將數據表中的資料提交到數據庫中。如果執行ROLLBACK命令,數據將會被回滾。如果不提交事務,而終止MySQL會話,數據庫將會自動執行回滾操作。
「MySQL的默認隔離級別是可重復讀(REPEATABLE READ)」。
事務的實現原理
我們要探究MySQL中事務的實現原理,實際上就是要弄明天它的ACID特性是如何實現的,在這里有必要先說明的是,「ACID中的一致性是事務的最終目標,前面提到的原子性、持久性和隔離性,都是為了保證數據庫狀態的一致性」。所以我們要分析的就是MySQL的原子性、持久性和隔離性的實現原理,在分析事務的實現原理之前我們需要補充一些InnoDB的相關知識
InnoDB是一個將表中的數據存儲到磁盤上的存儲引擎,所以即使關機后重啟我們的數據還是存在的。而真正「處理數據的過程是發生在內存中的」,「所以需要把磁盤中的數據加載到內存中,如果是處理寫入或修改請求的話,還需要把內存中的內容刷新到磁盤上」。而我們知道讀寫磁盤的速度非常慢,和內存讀寫差了幾個數量級,所以當我們想從表中獲取某些記錄時,InnoDB存儲引擎需要一條一條的把記錄從磁盤上讀出來么?不,那樣會慢死,InnoDB采取的方式是:「將數據劃分為若干個頁,以頁作為磁盤和內存之間交互的基本單位,InnoDB中頁的大小一般為?16?KB。也就是在一般情況下,一次最少從磁盤中讀取16KB的內容到內存中,一次最少把內存中的16KB內容刷新到磁盤中。」
我們還需要對MySQL中的日志有一定了解。MySQL的日志有很多種,如二進制日志(bin log)、錯誤日志、查詢日志、慢查詢日志等,此外InnoDB存儲引擎還提供了兩種事務日志:「redo log(重做日志)和undo log(回滾日志)。其中redo log用于保證事務持久性;undo log則是事務原子性和隔離性實現的基礎。」
InnoDB作為MySQL的存儲引擎,數據是存放在磁盤中的,但如果每次讀寫數據都需要磁盤IO,效率會很低。為此,InnoDB提供了「緩存(Buffer Pool)」,Buffer Pool中包含了磁盤中部分數據頁的映射,作為訪問數據庫的緩沖:「當從數據庫讀取數據時,會首先從Buffer Pool中讀取,如果Buffer Pool中沒有,則從磁盤讀取后放入Buffer Pool;當向數據庫寫入數據時,會首先寫入Buffer Pool,Buffer Pool中修改的數據會定期刷新到磁盤中(這一過程稱為刷臟)。」
InnoDB存儲引擎文件主要可以分為兩類,表空間文件及重做日志文件(redo log file),表空間文件又可以細分為兩類,共享表空間跟獨立表空間。「undo log位于共享表空間中的undo段中」,每個表空間都被劃分成了若干個頁面,「凡是頁面的讀寫都在buffer pool中進行,這意味著undo log也需要先寫入到buffer pool,所以undo log的生成也需要持久化,也就是說undo log的生成需要記錄對應的redo log」。(注意:不是所有的undo log的生成都會產生對應的redo log,對于操作臨時表生成的undo log并不會生成對應的undo log,因為修改臨時表而產生的undo日志只需要在系統運行過程中有效,如果系統奔潰了,那么在重啟時也不需要恢復這些undo日志所在的頁面,所以在寫針對臨時表的Undo頁面時,并不需要記錄相應的redo日志。)
持久性實現原理
通過前面的補充知識我們知道InnoDB引入了Buffer Pool來優化讀寫的性能,但是雖然Buffer Pool優化了性能,但同時也帶來了新的問題:「如果MySQL宕機,而此時Buffer Pool中修改的數據還沒有刷新到磁盤,就會導致數據的丟失,事務的持久性無法保證」。
基于此,redo log就誕生了,「redo log是物理日志,記錄的是數據庫中物理頁的情況」,redo log包括兩部分:一是內存中的日志緩沖(redo log buffer),該部分日志是易失性的;二是磁盤上的重做日志文件(redo log file),該部分日志是持久的。在概念上,innodb通過「force log at commit」機制實現事務的持久性,即在事務提交的時候,必須先將該事務的所有事務日志寫入到磁盤上的redo log file和undo log file中進行持久化。
看到這里可能有的小伙伴又會有疑問了,既然redo log也需要在事務提交時將日志寫入磁盤,為什么它比直接將Buffer Pool中修改的數據寫入磁盤(即刷臟)要快呢?主要有以下兩方面的原因:
(1)刷臟是隨機IO,因為每次修改的數據位置隨機,但寫redo log是追加操作,屬于順序IO。
(2)刷臟是以數據頁(Page)為單位的,MySQL默認頁大小是16KB,一個Page上一個小修改都要整頁寫入;而redo log中只包含真正需要寫入的部分,無效IO大大減少。
這里我以文章開頭的例子進行說明redo log為何能保證持久性:
//?第一步:開始事務 start?transaction; //?第二步:A賬戶余額減少減少1000?? update?balance?set?money?=?money?-1000?where?name=?‘A’; //?第三步:B賬戶余額增加1000?? update?balance?set?money?=?money?+1000?where?name=?‘B’; //?第四步:提交事務 commit; redo?
?這里需要對redo log的刷盤補充一點內容:
MySQL支持用戶自定義在commit時如何將log buffer中的日志刷log file中。這種控制通過變量 innodb_flush_log_at_trx_commit 的值來決定。該變量有3種值:0、1、2,「默認為1」。但注意,這個變量只是控制commit動作是否刷新log buffer到磁盤。
-
當設置為1的時候,事務每次提交都會將log buffer中的日志寫入os buffer并調用fsync()函數刷到log file on disk中。這種方式即使系統崩潰也不會丟失任何數據,但是因為每次提交都寫入磁盤,IO的性能較差。
-
當設置為0的時候,事務提交時不會將log buffer中日志寫入到os buffer(內核緩沖區),而是每秒寫入os buffer并調用fsync()寫入到log file on disk中。也就是說設置為0時是(大約)每秒刷新寫入到磁盤中的,當系統崩潰,會丟失1秒鐘的數據。
-
當設置為2的時候,每次提交都僅寫入到os buffer,然后是每秒調用fsync()將os buffer中的日志寫入到log file on disk。
「可以看到設置為0或者2時,都有可能丟失1s的數據」
?原子性實現原理
前面提到了,所謂原子性就是指整個事務是一個不可分隔的整體,組成事務的一組SQL要么全部成功,要么全部失敗,要達到這個目的就意味著當某一個SQL執行失敗時,我們要能夠撤銷掉其它SQL的執行結果,在MySQL中這是依賴undo log(回滾日志)來實現。
undo log屬于「邏輯日志」(前面提到的redo log屬于物理日志,記錄的是數據頁的情況),我們可以這么認為,「當delete一條記錄時,undo log中會記錄一條對應的insert記錄,反之亦然,當update一條記錄時,它記錄一條對應相反的update記錄。」
但執行發生異常時,會根據undo log中的記錄進行回滾。undo log主要分為兩種
insert undo log
update undo log
「insert undo log是指在insert 操作中產生的undo log」,因為insert操作的記錄,只對事務本身可見,對其他事務不可見。故該undo log可以在事務提交后直接刪除,不需要進行purge操作。
「而update undo log記錄的是對delete 和update操作產生的undo log」,該undo log可能需要提供MVCC機制,因此不能在事務提交時就進行刪除。提交時放入undo log鏈表,等待purge線程進行最后的刪除。
?補充:purge線程兩個主要作用是:清理undo頁和清除page里面帶有Delete_Bit標識的數據行。在InnoDB中,事務中的Delete操作實際上并不是真正的刪除掉數據行,而是一種Delete Mark操作,在記錄上標識Delete_Bit,而不刪除記錄。是一種"假刪除",只是做了個標記,真正的刪除工作需要后臺purge線程去完成。
?這里我們就來看看insert undo log的結構,如下:
insert undo在上圖中,undo type記錄的是undo log的類型,對于insert undo log,該值始終為11(TRX_UNDO_INSERT_REC),undo no在一個事務中是從0開始遞增的,也就是說只要事務沒提交,每生成一條undo日志,那么該條日志的undo no就增1。table id記錄undo log所對應的表對象。如果記錄中的主鍵只包含一個列,那么在類型為TRX_UNDO_INSERT_REC的undo日志中只需要把該列占用的存儲空間大小和真實值記錄下來,如果記錄中的主鍵包含多個列(復合主鍵),那么每個列占用的存儲空間大小和對應的真實值都需要記錄下來(圖中的len就代表列占用的存儲空間大小,value就代表列的真實值),「在回滾時只需要根據主鍵找到對應的列然后刪除即可」。end of record記錄了下一條undo log在頁面中開始的地址,start of record記錄了本條undo log在頁面中開始的地址。
對undo log有一定了解后,我們再回頭看看文章開頭的例子,分析下為什么undo log能保證原子性
//?第一步:開始事務 start?transaction; //?第二步:A賬戶余額減少1000?? update?balance?set?money?=?money?-1000?where?name=?‘A’; //?第三步:B賬戶余額增加1000?? update?balance?set?money?=?money?+1000?where?name=?‘B’; //?第四步:提交事務 commit; undo redo?
考慮到排版,這里我只畫了一條語句的流程圖,第二條也是一樣的,每次更新或者插入前,先記錄undo,再修改內存中數據,再記錄redo。
隔離性實現原理
我們知道,一個事務中的讀操作是不會影響到另外一個事務的,所以在討論隔離性我們主要分為兩種情況
一個事務中的寫操作,對另外一個事務中寫操作的影響
一個事務中的寫操作,對另外一個事務中讀操作的影響
寫操作之間的隔離是通過鎖來實現的,MySQL中的鎖機制要詳細來講是很復雜的,要講明白整個鎖需要從索引開始介紹,限于筆者能力及文章篇幅,本文只對MySQL中的鎖機制做一個簡單的介紹
MySQL中的鎖機制(InnoDB)
讀鎖跟寫鎖
讀鎖又稱為共享鎖`,簡稱S鎖,顧名思義,共享鎖就是多個事務對于同一數據可以共享一把鎖,「都能訪問到數據,但是只能讀不能修改。」
寫鎖又稱為排他鎖`,簡稱X鎖,顧名思義,排他鎖就是不能與其他鎖并存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取了排他鎖的事務是可以對數據進行讀取和修改的。
行鎖跟表鎖
表鎖在操作數據時會鎖定整張表,并發性能較差;
行鎖則只鎖定需要操作的數據,并發性能好。
但是由于加鎖本身需要消耗資源(獲得鎖、檢查鎖、釋放鎖等都需要消耗資源),因此在鎖定數據較多情況下使用表鎖可以節省大量資源。MySQL中不同的存儲引擎支持的鎖是不一樣的,例如MyIsam只支持表鎖,而InnoDB同時支持表鎖和行鎖,且出于性能考慮,絕大多數情況下使用的都是行鎖。
意向鎖
意向鎖分為兩種,意向讀鎖(IS)跟意向寫鎖(IX)
意向鎖是表級別的鎖
為什么需要意向鎖呢?思考一個問題:如果我們想對某個表加一個表鎖,那么在加鎖之前我們需要去檢查表中的每一行記錄是否已經被單獨加了行鎖,這樣的話豈不是意味著我們需要去遍歷表中所有的記錄依次進行檢查,遍歷是不可能的,這輩子都不可能遍歷的,基于效率的考慮,我們可以在每次給行記錄加鎖時先給當前表加一個意向鎖,如果我們要對行加讀鎖(S)的話,那么就先給表加一個意向讀鎖(IS),如果要對行加寫鎖(X)的話,那么先給表加一個意向寫鎖(IX),這樣當我們需要給整個表加鎖的時候就可以通過先判斷表上是否已經存在了意向鎖來決定是否可以上鎖了,避免遍歷,提高了效率。
意向鎖跟普通的讀鎖寫鎖間的兼容性如下:
| IS | 兼容 | 兼容 | 兼容 | 不兼容 |
| IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
| S | 兼容 | 不兼容 | 兼容 | 不兼容 |
| X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
注:IS(意向讀鎖/意向共享鎖), ?IX(意向寫鎖/意向排他鎖), ?S(讀鎖/共享鎖),X(寫鎖/排他鎖)
從上圖中可以看出,意向鎖之間都是兼容的,這是因為意向鎖的作用僅僅是來快速判斷是否可以直接上表鎖。
「接下來介紹的這幾種鎖都屬于行鎖」,為了更好的理解這幾種鎖,我們先創建一個表
CREATE?TABLE?`user`?(`id`?int(11)?NOT?NULL?AUTO_INCREMENT,`name`?varchar(10)?NOT?NULL,PRIMARY?KEY?(`id`), )?ENGINE=InnoDB?AUTO_INCREMENT=7?DEFAULT?CHARSET=utf8;其中id為主鍵,沒有建其余的索引,插入如下數據
INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(1,?'a張大膽'); INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(3,?'b王翠花'); INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(6,?'c范統'); INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(8,?'d朱逸群'); INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(15,?'e董格求');Record Lock(記錄鎖)
鎖定單條記錄
也分為S鎖跟X鎖
如果我們對id為3的記錄添加一個行鎖,對應如下(圖中每一列代表數據庫中的一行記錄):
行鎖Gap Lock(間隙鎖)
鎖定一個范圍,但是不包含記錄本身
間隙鎖的主要作用在于防止幻讀的發生,雖然也有S鎖跟X鎖的區分,但是它們的作用都是相同的,而且如果你對一條記錄加了間隙鎖(不論是共享間隙鎖還是獨占間隙鎖),并不會限制其他事務對這條記錄加記錄鎖或者繼續加間隙鎖,再強調一遍,間隙鎖的作用僅僅是為了防止幻讀的發生。
假設我們要對id為6的記錄添加間隙鎖,那么此時鎖定的區域如下所示
其中虛線框代表的是要鎖定的間隙,其實就是當前需要加間隙鎖的記錄跟上一條記錄之間的范圍,但是間隙鎖不會鎖定當前記錄,如圖所示,id=6的記錄并沒有被加鎖。(圖中虛線框表示鎖間隙,沒有插入真實的記錄)
間隙鎖Next-Key Lock(Gap Lock+Record Lock)
假設我們要對id為6的記錄添加Next-Key Lock,那么此時鎖定的區域如下所示
next key lock跟間隙鎖最大的區別在于,Next-Key Lock除了鎖定間隙之外還要鎖定當前記錄
?通過鎖實現了寫、寫操作之間的隔離性,實際上我們也可以通過加鎖來實現讀、寫之間的隔離性,但是這樣帶來一個問題,讀、寫需要串行執行這樣會大大降低效率,所以MySQL中實現讀寫之間的隔離性是通過MVCC+鎖來實現的,對于讀采用快照,對于寫使用加鎖!
?MVCC(多版本并發控制)
版本鏈
在介紹MVCC之前我們需要對MySQL中的行記錄格式有一定了解,其實除了我們在數據庫中定義的列之外,每一行中還包含了幾個隱藏列,分別是
-
row_id:行記錄的唯一標志
-
transaction_id:事務ID
-
roll_pointer:回滾指針
「row_id是行記錄的唯一標志,這一列不是必須的。」
MySQL會優先使用用戶自定義主鍵作為主鍵,如果用戶沒有定義主鍵,則選取一個Unique鍵作為主鍵,如果表中連Unique鍵都沒有定義的話,則InnoDB會為表默認添加一個名為row_id的隱藏列作為主鍵。也就是說只有在表中既沒有定義主鍵,也沒有申明唯一索引的情況MySQL才會添加這個隱藏列。
「transaction_id代表的是事務的ID」。當一個事務對某個表執行了增、刪、改操作,那么InnoDB存儲引擎就會給它分配一個獨一無二的事務id,分配方式如下:
-
對于只讀事務來說,只有在它第一次對某個用戶創建的「臨時表執行增、刪、改操作」時才會為這個事務分配一個事務id,否則的話是不分配事務id的。
-
對于讀寫事務來說,只有在它「第一次對某個表(包括用戶創建的臨時表)執行增、刪、改操作」時才會為這個事務分配一個事務id,否則的話也是不分配事務id的。
有的時候雖然我們開啟了一個讀寫事務,但是在這個事務中全是查詢語句,并沒有執行增、刪、改的語句,那也就意味著這個事務并不會被分配一個事務id。
「roll_pointer表示回滾指針,指向該記錄對應的undo log」。前文已經提到過了,undo log記錄了對應記錄在修改前的狀態,通過roll_pointer我們就可以找到對應的undo log,然后根據undo log進行回滾。
?在之前介紹undo log的時候我們只介紹了insert undo log的數據格式,實際上除了insert undo log還有update undo log,而update undo log中也包含roll_pointer跟transaction_id。update undo log中的roll_pointer指針其實就是保存的被更新的記錄中的roll_pointer指針
?「除了這些隱藏列以外,實際上每條記錄的記錄頭信息中還會存儲一個標志位,標志該記錄是否刪除。」
我們以實際的例子來說明上面三個隱藏列的作用,還是以之前的表為例,現在對其執行如下SQL:
#?開啟事務 START?TRANSACTION; #?插入一條數據 INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(16,?'e杜子騰'); #?更新插入的數據 UPDATE?`test`.`user`?SET?name?=?"史珍香"?WHERE?id?=?16; #?刪除數據 DELETE?from??`test`.`user`?WHERE?id?=?16;我們通過畫圖來看看上面這段SQL在執行的過程中都做了什么
SQL執行流程圖從上圖中我們可以看到,每對記錄進行一次增、刪、改時,都會生成一條對應的undo log,并且被修改后的記錄中的roll pointer指針指向了這條undo log,同時如果不是新增操作,那么生成的undo log中也會保存一個roll pointer,其值是從被修改的數據中復制過來了,在我們上邊的例子中update undo log的roll pointer就復制了insert進去的數據中的roll pointer指針的值。
另外我們會發現,根據當前記錄中的roll pointer指針,我們可以找到一個有undo log組成的鏈表,這個undo log鏈表其實就是這條記錄的版本鏈。
ReadView(快照)
對于使用READ UNCOMMITTED隔離級別的事務來說,由于可以讀到未提交事務修改過的記錄,所以直接讀取記錄的最新版本就好了;
對于使用SERIALIZABLE隔離級別的事務來說,MySQL規定使用加鎖的方式來訪問記錄;
對于使用READ COMMITTED和REPEATABLE READ隔離級別的事務來說,都必須保證讀到已經提交了的事務修改過的記錄,也就是說假如另一個事務已經修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是:「需要判斷一下版本鏈中的哪個版本是當前事務可見的」。
為了解決這個問題,MySQL提出了一個ReadView(快照)的概念,「在Select操作前會為當前事務生成一個快照,然后根據快照中記錄的信息來判斷當前記錄是否對事務是可見的,如果不可見那么沿著版本鏈繼續往上找,直至找到一個可見的記錄。」
「ReadView」(快照)中包含了下面幾個關鍵屬性:
-
m_ids:表示在生成ReadView時當前系統中活躍的讀寫事務的事務id列表。
-
min_trx_id:表示在生成ReadView時當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值。
-
max_trx_id:表示生成ReadView時系統中應該分配給下一個事務的id值。
?小貼士:注意max_trx_id并不是m_ids中的最大值,事務id是遞增分配的。比方說現在有id為1,2,3這三個事務,之后id為3的事務提交了。那么一個新的讀事務在生成ReadView時,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
? -
creator_trx_id:表示生成該ReadView的事務的事務id。
?小貼士:我們前邊說過,只有在對表中的記錄做改動時(執行INSERT、DELETE、UPDATE這些語句時)才會為事務分配事務id,否則在一個只讀事務中的事務id值都默認為0。
?
當生成快照后,會通過下面這個流程來判斷該記錄對當前事務是否可見
MVCC?
從上圖中我們可以看到,在根據當前數據庫中運行中的讀寫事務id,會去生成一個ReadView。
然后根據要讀取的數據記錄中的事務id(方便區別,記為r_trx_id)跟ReadView中保存的幾個屬性做如下判斷
-
如果被訪問版本的r_trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當前事務在訪問它自己修改過的記錄,所以該版本可以被當前事務訪問。
-
如果被訪問版本的r_trx_id屬性值小于ReadView中的min_trx_id值,表明生成該版本的事務在當前事務生成ReadView前已經提交,所以該版本可以被當前事務訪問。
-
如果被訪問版本的r_trx_id屬性值大于或等于ReadView中的max_trx_id值,表明生成該版本的事務在當前事務生成ReadView后才開啟,所以該版本不可以被當前事務訪問。
-
如果被訪問版本的r_trx_id屬性值在ReadView的min_trx_id和max_trx_id之間,那就需要判斷一下r_trx_id屬性值是不是在m_ids列表中,如果在,說明創建ReadView時生成該版本的事務還是活躍的,該版本不可以被訪問;如果不在,說明創建ReadView時生成該版本的事務已經被提交,該版本可以被訪問。
-
如果某個版本的數據對當前事務不可見的話,那就順著版本鏈找到下一個版本的數據,繼續按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最后一個版本。如果最后一個版本也不可見的話,那么就意味著該條記錄對該事務完全不可見,查詢結果就不包含該記錄。
實際上,提交讀跟可重復讀在實現上最大的差異就在于
提交讀每次select都會生成一個快照
可重復讀只有在第一次會生成一個快照
總結
本文主要介紹了事務的基本概念跟MySQL中事務的實現原理。下篇文章開始我們就要真正的進入Spring的事務學習啦!鋪墊了這么久,終于開始主菜了......
在前面的大綱里也能看到,會分為上下兩篇,第一篇講應用以及在使用過程中會碰到的問題,第二篇我們就深入源碼分析Spring中的事務機制的實現原理!
「參考」:
書籍:掘金小冊《MySQL 是怎樣運行的:從根兒上理解 MySQL》:https://juejin.im/book/6844733769996304392
書籍:《MySQL技術內幕:InnoDB存儲引擎》:關注公眾號,程序員DMZ,后臺回復InnoDB即可領取
書籍:《高性能MySQL》:關注公眾號,程序員DMZ,后臺回復MySQL即可領取
文章:《深入學習MySQL事務:ACID特性的實現原理》:https://www.cnblogs.com/kismetv/p/10331633.html
文章:《詳細分析MySQL事務日志(redo log和undo log)》:https://www.cnblogs.com/f-ck-need-u/p/9010872.html
文章:《Mysql事務實現原理》:https://www.lagou.com/lgeduarticle/82740.html
文章:《面試官:你說熟悉MySQL事務,那來談談事務的實現原理吧!》:https://mp.weixin.qq.com/s/jrfZr3YzE_E0l3KjWAz1aQ
文章:《InnoDB 事務分析-Undo Log》:http://leviathan.vip/2019/02/14/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-Undo-Log/
文章:《InnoDB 的 Redo Log 分析》:http://leviathan.vip/2018/12/15/InnoDB%E7%9A%84Redo-Log%E5%88%86%E6%9E%90/
文章:《MySQL redo & undo log-這一篇就夠了》:https://www.jianshu.com/p/336e4995b9b8
總結
以上是生活随笔為你收集整理的Spring事务专题(三)事务的基本概念,Mysql事务处理原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Boot 中的 RestT
- 下一篇: Spring事务专题(四)Spring中