Spring事務專題(三)事務的基本概念,Mysql事務處理原理

點擊藍色「程序員DMZ 」關注我喲html

好看記得加個「星標」哈!程序員

前言
web

本專題大綱:面試

專欄大綱

我從新整理了大綱,思考了好久,決定單獨將MySQL的事務實現原理跟Spring中的事務示例分爲兩篇文章,由於兩者畢竟沒有什麼實際關係,實際上若是你對MySQL的事務原理不感興趣也能夠直接跳過本文,等待接下來兩篇應用及源碼分析,不過我以爲知識的學習應該慢慢行成一個體系,爲了創建一個完善的體系應該要對數據庫自己事務的實現有必定認知才行。sql

本文爲Spring事務專題第三篇,在前兩篇文章中咱們已經對Spring中的數據訪問有了必定的瞭解,那麼從本文開始咱們正式接觸事務,在分析Spring中事務的實現以前咱們應該要對事務自己有必定的瞭解,同時也要對數據庫層面的事務如何實現有必定了解。話很少說,咱們開始正文數據庫

本文大綱:緩存

MYSQL事務大綱

初識事務

爲何須要事務?

這裏又要掏出那個爛大街的銀行轉帳案例了,以A、B兩個帳戶的轉帳爲例,假設如今要從A帳戶向B帳戶中轉入1000員,當進行轉帳時,須要先從銀行帳戶A中取出錢,而後再存入銀行帳戶B中,SQL樣本以下:微信

// 第一步:A帳戶餘額減小減小1000  
update balance set money = money -500 where name= ‘A’;
// 第二步:B帳戶餘額增長1000  
update balance set money = money +500 where name= ‘B’;

若是在完成了第1步的時候忽然宕機了,A的錢減小了而B的錢沒有增長,那A豈不是白白丟了1000元,這時候就須要用到咱們的事務了,開啓事務後SQL樣本以下:併發

// 第一步:開始事務
start transaction;
// 第二步:A帳戶餘額減小減小1000  
update balance set money = money -500 where name= ‘A’;
// 第三步:B帳戶餘額增長1000  
update balance set money = money +500 where name= ‘B’;
// 第四步:提交事務
commit;

什麼是事務

事務(Transaction)是訪問和更新數據庫的程序執行單元;事務中可能包含一個或多個sql語句,這些語句要麼都執行成功,要麼所有執行失敗。app

事務的四大特性(ACID)

  • 原子性(Atomicity,或稱不可分割性)

「一個事務必須被視爲一個不可分割的最小工做單元,整個事務中全部的操做要麼所有提交成功,要麼所有失敗回滾,對於一個事務來講,不可能只執行其中的一部分操做,這就是事務的原子性」

  • 一致性(Consistency)

「數據庫老是從一個一致性的狀態轉換到另一個一致性的狀態,在事務開始以前和以後,數據庫的完整性約束沒有被破壞。在前面的例子中,事務結束先後A、B帳戶總額始終保持不變」

  • 隔離性(Isolation)

「隔離性是指,事務內部的操做與其餘事務是隔離的,併發執行的各個事務之間不能互相干擾。嚴格的隔離性,對應了事務隔離級別中的Serializable (可串行化),但實際應用中出於性能方面的考慮不多會使用可串行化。」

  • 持久性(Durability)

「持久性是指事務一旦提交,它對數據庫的改變就應該是永久性的。接下來的其餘操做或故障不該該對其有任何影響。」

事務的隔離級別

在前文中咱們介紹了隔離性,但實際上隔離性比想象的要複雜的多。在SQL標準中定義了四種隔離級別,每一種隔離級別都規定了一個事務所作的修改,哪些在事務內和事務間是可見的,哪些是不可見的,較低級別的隔離一般能夠執行跟高的併發,系統的開銷也更低

未提交讀(READ UNCOMMITTED)

在這個隔離級別下,事務的修改即便沒有提交,對其餘事務也是可見的。事務能夠讀取未提交的數據,這也被稱之爲髒讀。這個級別會帶來不少問題,從性能上來講,READ UNCOMMITTED不會比其餘的級別好太多,可是卻會帶來不少問題,除非真的有很是必要的理由,在實際應用中通常不多使用。

提交讀(REDA COMMITED)

大多數數據系統的默認隔離級別都是REDA COMMITED(MySql不是),REDA COMMITED知足前面提到的隔離性的簡單定義:一個事務開始時,只能看到已經提交的事務所作的修改。換句話說,一個事物從開始直到提交前,所作的修改對其餘事務不可見。這個級別有時候也叫作不可重複讀,由於執行兩次相同的查詢可能會獲得不一樣的結果。

可重複讀(REPEATABLE READ)

REPEATABLE READ解決了髒讀以及不可重複度的問題。該級別保證了同一個事務屢次讀取一樣記錄的結果是一致的。可是理論上,可重複度仍是沒法解決另一個幻讀的問題。所謂幻讀,指的是當某個事務在讀取某個範圍內的記錄時,另一個事務又在該範圍內插入了新的記錄,當以前的事務再次讀取該範圍的記錄時,就會產生幻行。

不可重複讀跟幻讀的區別在於,「前者是數據發生了變化,後者是數據的行數發生了變化」

可串行化(SERIALIZABLE)

SERIALIZABLE是最高的隔離級別,它經過強制事務串行執行,避免前面說的幻讀。簡單來講SERIALIZABLE會在讀取的每一行數據上都加鎖,因此可能會致使大量的超時和鎖爭用的問題。實際應用中也不多使用這個隔離級別,只有在很是須要確保數據一致性並且能夠接受沒有併發的狀況下,才考慮此級別。

保存點

咱們能夠在事務執行的過程當中定義保存點,在回滾時直接指定回滾到指定的保存點而不是事務開始之初,有點像咱們玩遊戲的時候能夠存檔而不是每次都要從新再來

定義保存點的語法以下:

SAVEPOINT 保存點名稱;

當咱們想回滾到某個保存點時,可使用下邊這個語句(下邊語句中的單詞WORKSAVEPOINT是無關緊要的):

ROLLBACK [WORKTO [SAVEPOINT] 保存點名稱;

MySQL中的事務跟原理

MySQL中的事務

  1. 「MySQL中不是全部的存儲引擎都支持事務」,例如MyISAM就不支持事務,實際上支持事務的只有InnoDBNDB Cluster「本文關於事務的分析都是基於InnoDB

  2. 「MySQL默認採用的是自動提交的方式」,也就是說若是不是顯示的開始一個事務,則系統會自動向數據庫提交結果。在當前鏈接中,還能夠經過設置AUTOCONNIT變量來啓用或者禁用自動提交模式。

  • 開啓自動提交功能
SET AUTOCOMMIT = 1;

MySQL中默認狀況下的自動提交功能是已經開啓的。

  • 關閉自動提交功能。
SET AUTOCOMMIT = 0;

關閉自動提交功能後,只用當執行COMMIT命令後,MySQL纔將數據表中的資料提交到數據庫中。若是執行ROLLBACK命令,數據將會被回滾。若是不提交事務,而終止MySQL會話,數據庫將會自動執行回滾操做。

  1. 「MySQL的默認隔離級別是可重複讀(REPEATABLE READ)」

事務的實現原理

咱們要探究MySQL中事務的實現原理,實際上就是要弄明天它的ACID特性是如何實現的,在這裏有必要先說明的是,ACID中的一致性是事務的最終目標,前面提到的原子性、持久性和隔離性,都是爲了保證數據庫狀態的一致性」。因此咱們要分析的就是MySQL的原子性、持久性和隔離性的實現原理,在分析事務的實現原理以前咱們須要補充一些InnoDB的相關知識

  1. InnoDB是一個將表中的數據存儲到磁盤上的存儲引擎,因此即便關機後重啓咱們的數據仍是存在的。而真正「處理數據的過程是發生在內存中的」「因此須要把磁盤中的數據加載到內存中,若是是處理寫入或修改請求的話,還須要把內存中的內容刷新到磁盤上」。而咱們知道讀寫磁盤的速度很是慢,和內存讀寫差了幾個數量級,因此當咱們想從表中獲取某些記錄時,InnoDB存儲引擎須要一條一條的把記錄從磁盤上讀出來麼?不,那樣會慢死,InnoDB採起的方式是:「將數據劃分爲若干個頁,以頁做爲磁盤和內存之間交互的基本單位,InnoDB中頁的大小通常爲 16 KB。也就是在通常狀況下,一次最少從磁盤中讀取16KB的內容到內存中,一次最少把內存中的16KB內容刷新到磁盤中。」

  2. 咱們還須要對MySQL中的日誌有必定了解。MySQL的日誌有不少種,如二進制日誌(bin log)、錯誤日誌、查詢日誌、慢查詢日誌等,此外InnoDB存儲引擎還提供了兩種事務日誌:「redo log(重作日誌)和undo log(回滾日誌)。其中redo log用於保證事務持久性;undo log則是事務原子性和隔離性實現的基礎。」

  3. InnoDB做爲MySQL的存儲引擎,數據是存放在磁盤中的,但若是每次讀寫數據都須要磁盤IO,效率會很低。爲此,InnoDB提供了「緩存(Buffer Pool)」,Buffer Pool中包含了磁盤中部分數據頁的映射,做爲訪問數據庫的緩衝:「當從數據庫讀取數據時,會首先從Buffer Pool中讀取,若是Buffer Pool中沒有,則從磁盤讀取後放入Buffer Pool;當向數據庫寫入數據時,會首先寫入Buffer Pool,Buffer Pool中修改的數據會按期刷新到磁盤中(這一過程稱爲刷髒)。」

  4. 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 -500 where name= ‘A’;
// 第三步:B帳戶餘額增長1000  
update balance set money = money +500 where name= ‘B’;
// 第四步:提交事務
commit;
redo

這裏須要對redo log的刷盤補充一點內容:

MySQL支持用戶自定義在commit時如何將log buffer中的日誌刷log file中。這種控制經過變量 innodb_flush_log_at_trx_commit 的值來決定。該變量有3種值:0、一、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主要分爲兩種

  1. insert undo log
  2. 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_RECundo日誌中只須要把該列佔用的存儲空間大小和真實值記錄下來,若是記錄中的主鍵包含多個列(複合主鍵),那麼每一個列佔用的存儲空間大小和對應的真實值都須要記錄下來(圖中的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 -500 where name= ‘A’;
// 第三步:B帳戶餘額增長1000  
update balance set money = money +500 where name= ‘B’;
// 第四步:提交事務
commit;
undo redo

考慮到排版,這裏我只畫了一條語句的流程圖,第二條也是同樣的,每次更新或者插入前,先記錄undo,再修改內存中數據,再記錄redo。

隔離性實現原理

咱們知道,一個事務中的讀操做是不會影響到另一個事務的,因此在討論隔離性咱們主要分爲兩種狀況

  1. 一個事務中的寫操做,對另一個事務中寫操做的影響
  2. 一個事務中的寫操做,對另一個事務中讀操做的影響

寫操做之間的隔離是經過鎖來實現的,MySQL中的鎖機制要詳細來說是很複雜的,要講明白整個鎖須要從索引開始介紹,限於筆者能力及文章篇幅,本文只對MySQL中的鎖機制作一個簡單的介紹

MySQL中的鎖機制(InnoDB)

讀鎖跟寫鎖
  1. 讀鎖又稱爲共享鎖`,簡稱S鎖,顧名思義,共享鎖就是多個事務對於同一數據能夠共享一把鎖,「都能訪問到數據,可是隻能讀不能修改。」

  2. 寫鎖又稱爲排他鎖`,簡稱X鎖,顧名思義,排他鎖就是不能與其餘所並存,如一個事務獲取了一個數據行的排他鎖,其餘事務就不能再獲取該行的其餘鎖,包括共享鎖和排他鎖,可是獲取排他鎖的事務是能夠對數據就行讀取和修改。

行鎖跟表鎖
  1. 表鎖在操做數據時會鎖定整張表,併發性能較差;

  2. 行鎖則只鎖定須要操做的數據,併發性能好。

  3. 可是因爲加鎖自己須要消耗資源(得到鎖、檢查鎖、釋放鎖等都須要消耗資源),所以在鎖定數據較多狀況下使用表鎖能夠節省大量資源。MySQL中不一樣的存儲引擎支持的鎖是不同的,例如MyIsam只支持表鎖,而InnoDB同時支持表鎖和行鎖,且出於性能考慮,絕大多數狀況下使用的都是行鎖。

意向鎖
  1. 意向鎖分爲兩種,意向讀鎖(IS)跟意向寫鎖(IX)

  2. 意向鎖是表級別的鎖

  3. 爲何須要意向鎖呢?思考一個問題:若是咱們想對某個表加一個表鎖,那麼在加鎖以前咱們須要去檢查表中的每一行記錄是否已經被單獨加了行鎖,這樣的話豈不是意味着咱們須要去遍歷表中全部的記錄依次進行檢查,遍歷是不可能的,這輩子都不可能遍歷的,基於效率的考慮,咱們能夠在每次給行記錄加鎖時先給當前表加一個意向鎖,若是咱們要對行加讀鎖(S)的話,那麼就先給表加一個意向讀鎖(IS),若是要對行加寫鎖(X)的話,那麼先給表加一個意向寫鎖(IX),這樣當咱們須要給整個表加鎖的時候就能夠經過先判斷表上是否已經存在了意向鎖來決定是否能夠上鎖了,避免遍歷,提升了效率。

  4. 意向鎖跟普通的讀鎖寫鎖間的兼容性以下:


IS IX S X
IS 兼容 兼容 兼容 不兼容
IX 兼容 兼容 不兼容 不兼容
S 兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

注:IS(意向讀鎖/意向共享鎖),  IX(意向寫鎖/意向排他鎖),  S(讀鎖/共享鎖),X(寫鎖/排他鎖)

從上圖中能夠看出,意向鎖之間都是兼容的,這是由於意向鎖的做用僅僅是來快速判斷是否能夠直接上表鎖。


「接下來介紹的這幾種鎖都屬於行鎖」,爲了更好的理解這幾種鎖,咱們先建立一個表

CREATE TABLE `user` (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `name` varchar(10NOT 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(記錄鎖)
  1. 鎖定單條記錄
  2. 也分爲S鎖跟X鎖

若是咱們對id爲3的記錄添加一個行鎖,對應以下(圖中每一列表明數據庫中的一行記錄):

行鎖
Gap Lock(間隙鎖)
  1. 鎖定一個範圍,可是不包含記錄自己
  2. 間隙鎖的主要做用在於防止幻讀的發生,雖然也有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_pointertransaction_idupdate 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 COMMITTEDREPEATABLE 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
  1. 從上圖中咱們能夠看到,在根據當前數據庫中運行中的讀寫事務id,會去生成一個ReadView。
  2. 而後根據要讀取的數據記錄中的事務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屬性值在 ReadViewmin_trx_idmax_trx_id之間,那就須要判斷一下 r_trx_id屬性值是否是在 m_ids列表中,若是在,說明建立 ReadView時生成該版本的事務仍是活躍的,該版本不能夠被訪問;若是不在,說明建立 ReadView時生成該版本的事務已經被提交,該版本能夠被訪問。
  • 若是某個版本的數據對當前事務不可見的話,那就順着版本鏈找到下一個版本的數據,繼續按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最後一個版本。若是最後一個版本也不可見的話,那麼就意味着該條記錄對該事務徹底不可見,查詢結果就不包含該記錄。

實際上,提交讀跟可重複讀在實現上最大的差別就在於

  1. 提交讀每次select都會生成一個快照
  2. 可重複讀只有在第一次會生成一個快照

總結

本文主要介紹了事務的基本概念跟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

本文就到這裏啦,若是本文對你有幫助的話,記得幫忙三連哈,感謝~!

我叫DMZ,一個在學習路上匍匐前行的小菜鳥!


往期精選


Spring官網閱讀筆記

Spring雜談

JVM系列文章

Spring源碼專題


本文分享自微信公衆號 - 程序員DMZ(programerDmz)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索