MySQL中的事務和MVCC

本篇博客參考掘金小冊——MySQL 是怎樣運行的:從根兒上理解 MySQL數據庫

雖然咱們不是DBA,可能對數據庫沒那麼瞭解,可是對於數據庫中的索引、事務、鎖,咱們仍是必需要有一個較爲淺顯的認識,今天我就和你們聊聊事務。bash

爲何要有事務

說到事務,不得不提到轉帳的事情,幾乎全部的關於事務的文章都會提到這個老掉牙的案例,我也不例外。服務器

轉帳在數據庫層面能夠簡單的抽象成兩個部分:session

  • 從本身的帳戶中扣除轉帳金額;
  • 往對方帳戶中增長轉帳金額。

若是先從本身的帳戶中扣除轉帳金額,再往對方帳戶中增長轉帳金額,扣除執行成功,增長執行失敗,那本身的帳戶白白少了100塊,欲哭無淚。架構

若是先往對方帳戶中增長轉帳金額,再從本身的帳戶中扣除轉帳金額,增長執行成功,扣除執行失敗,那對方帳戶白白增長了100塊,本身的帳戶也沒有扣錢,喜大普奔。併發

不論是讓你欲哭無淚,仍是喜大普奔,銀行都不會容忍這樣的事情發生,他們會引入事務來解決這類問題。性能

事務的特性

  1. 原子性(Atomicity):事務包含的全部操做要麼所有成功(提交),要麼所有失敗(回滾)。
  2. 一致性(Consistency):事務的執行的先後數據的完整性保持一致。
  3. 隔離性(Isolation):一個事務執行的過程當中,不該該受到其餘事務的干擾。
  4. 持久性(Durability):事務一旦結束,數據就持久到數據庫,即便提交後,數據庫發生崩潰,也不會丟失提交的數據。

四種特性,簡稱ACID,其中最很差理解的就是一致性,有很多人認爲原子性、隔離性、持久性就是爲了保證一致性,咱們也不搞學術研究,一致性到底該怎麼解釋,到底怎麼定義一致性,就看各位看官的了。spa

事務的隔離級別

從某個角度來講,咱們能夠控制的、或者說須要研究的只有隔離性這一個特性,而要控制隔離性,幾乎只有調整隔離級別這一個手段,下面咱們就來看看事務的隔離級別。翻譯

數據庫是一個客戶端/服務器架構的軟件,每一個客戶端與服務器鏈接後,就會產生一個session(會話),客戶端和服務器的交互就是在session中進行的,理論上來講,若是服務器同時只能處理一個事務,其餘的事務都排隊等待,當該事務提交後,服務器才處理下一個事務,這樣才真正具備「隔離性」,什麼問題都沒有了,可是若是是這樣,性能就太差了,在性能和隔離性之間,只能作一些平衡,因此數據庫提供了好幾個隔離級別供咱們選擇。指針

在講隔離級別以前,咱們先來看看事務併發執行會遇到什麼問題。

爲了保證下面的敘述能夠順利進行,咱們要先建一張表:

CREATE TABLE `student` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年齡',
  `grade` int(11) DEFAULT NULL COMMENT '年級',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
複製代碼

髒寫

image.png
如圖所示:

  1. sessionA和sessionB開啓了一個事務;
  2. sessionB把id=2的name修改爲了「地底王」;
  3. sessionA把id=2的name修改爲了「夢境地底王」;
  4. sessionB回滾了事務;
  5. sessionA提交了事務。

若是sessionB在回滾事務的時候把sessionA的修改也給回滾了,致使sessionA的提交丟失了,這種現象就被稱爲「髒寫」。sessionA會一臉懵逼,我明明修改了數據,也提交了數據,爲何數據沒有變化呢。

髒讀

image.png
如圖所示:

  1. sessionA和sessionB開啓了一個事務;
  2. sessionB把id=2的name修改爲了「地底王」,此時還未提交;
  3. sessionA查詢了id=2的數據,若是讀出來的數據的name是「地底王」,也就是讀到了sessionB尚未提交的數據,就被稱爲「髒讀」。

不可重複讀

image.png
如圖所示:

  1. sessionA和sessionB開啓了一個事務;
  2. sessionA查詢id=2的數據,假如name是「地底王」,
  3. sessionB把id=2的name修改爲了「夢境地底王」,隨後提交了事務;
  4. sessionA再一次查詢了id=2的數據,若是name是「夢境地底王」,說明在同一個事務中,sessionA先後讀到的數據不一致,就被稱爲「不可重複讀」。

幻讀

image.png
如圖所示:

  1. sessionA和sessionB開啓了一個事務;
  2. sessionA查詢name=「地底王」的數據,假設此時讀到了一條記錄;
  3. sessionB又插入一條name=「地底王」的數據,隨後提交;
  4. seesionA再一次查詢name=「地底王」的數據,若是此時讀到了兩條記錄,第二次查詢讀到了第一次查詢未查詢出來的數據,就被稱爲「幻讀」。

四種隔離級別

咱們知道了在併發執行事務的時候,會遇到什麼問題,有些問題比較嚴重,有些問題比較輕微,通常來講,咱們認爲按照嚴重性排序是這樣的:

髒寫>髒讀>不可重複讀>幻讀

在SQL標準定義中,設定了四種隔離級別,來解決上述的問題:

  • 未提交讀(READ UNCOMMITTED): 最低的隔離級別,會有「髒讀」、「不可重複讀」,「幻讀」三個問題。
  • 讀已提交(READ COMMITTED): SQLServer默認隔離級別,能夠避免「髒讀」,會有「不可重複讀」,「幻讀」兩個問題。
  • 可重複讀(REPEATABLE READ): 能夠避免「髒讀」,「不可重複讀」兩個問題,會有「幻讀」問題。 MySQL默認隔離級別,可是在MySQL中,此隔離級別解決了「幻讀」問題。
  • 串行化(SERIALIZABLE): 全部的問題都不會發生。

由於髒寫的問題實在太嚴重了,在任何隔離級別下,都不會有髒寫的問題。

MVCC

前面說的都是開胃菜,相信大部分小夥伴對於上述內容都是手到擒來,因此我連如何修改事務隔離級別都沒有介紹,各類實驗也都沒有作,就是要把大量的時間、文字投入到這一部份內容中來。

MVCC,全稱是Mutil-Version Concurrency Control,翻譯成中文是多版本併發控制,MySQL就利用了MVCC來判斷在一個事務中,哪一個數據能夠被讀出來,哪一個數據不能被讀出來。

多版本

在看MVCC以前,咱們有必要知道另一個知識點,數據庫存儲一行行數據,是分爲兩個部分來存儲的,一個是數據行的額外信息(本篇博客不涉及),一個是真實的數據記錄,MySQL會爲每一行真實數據記錄添加兩三個隱藏的字段:

  • row_id 非必須,若是表中有自定義的主鍵或者有Unique鍵,就不會添加row_id字段,若是二者都沒有,MySQL會「自做主張」添加row_id字段。
  • transaction_id 必須,事務Id,表明這一行數據是由哪一個事務id建立的。
  • roll_pointer 必須,回滾指針,指向這行數據的上一個版本。

以下圖所示:

image.png

在這裏須要着重說明下事務id,當咱們開啓一個事務,並不會立刻得到事務id,哪怕咱們在事務中執行select語句,也是沒有事務id的(事務id爲0),只有執行insert/update/delete語句才能得到事務id,這一點尤其重要。

其中和MVCC緊密相關的是transaction_id和roll_pointer兩個字段,在開發過程當中,咱們無需關心,可是要研究MVCC,咱們必須關心。

若是有相似這樣的一行數據:

image.png
表明這行數據是由transaction_id爲9的事務建立出來的,roll_pointer是空的,由於這是一條新紀錄。

實際上,roll_pointer並非空的,若是真要解釋,須要繞一大圈,理解成空的,問題也不大。

當咱們開啓事務,對這條數據進行修改,會變成這樣:

image.png

有點感受了吧,這就像一個單向鏈表,稱之爲「版本鏈」,最上面的數據是這個數據的最新版本,roll_pointer指向這個數據的舊版本,給人的感受就是一行數據有多個版本,是否是符合「多版本併發控制」中的「多版本」這個概念, 那麼「併發控制」又是怎麼作到的呢,別急,繼續往下看。

ReadView

哎,下面又要引出一個新的概念:ReadView。

對於READ UNCOMMITTED來講,能夠讀取到其餘事務尚未提交的數據,因此直接把這個數據的最新版本讀出來就能夠了,對於SERIALIZABLE來講,是用加鎖的方式來訪問記錄。

剩下的就是READ COMMITTED和REPEATABLE READ,這兩個事務隔離級別都要保證讀到的數據是其餘事務已經提交的,也就是不能無腦把一行數據的最新版本給讀出來了,可是這兩個仍是有必定的區別,最核心的問題就在於「我到底能夠讀取這個數據的哪一個版本」。

爲了解決這個問題,ReadView的概念就出現了,ReadView包含四個比較重要的內容:

  • m_ids:表示在生成ReadView時,系統中活躍的事務id集合。
  • min_trx_id:表示在生成ReadView時,系統中活躍的最小事務id,也就是 m_ids中的最小值。
  • max_trx_id:表示在生成ReadView時,系統應該分配給下一個事務的id。
  • creator_trx_id:表示生成該ReadView的事務id。

有了這個ReadView,只要按照下面的判斷方式就能夠解決「我到底能夠讀取這個數據的哪一個版本」這個千古難題了:

  • 若是被訪問的版本的trx_id和ReadView中的creator_trx_id相同,就意味着當前版本就是由你「形成」的,能夠讀出來。
  • 若是被訪問的版本的trx_id小於ReadView中的min_trx_id,表示生成該版本的事務在建立ReadView的時候,已經提交了,因此該版本能夠讀出來。
  • 若是被訪問版本的trx_id大於或等於ReadView中的max_trx_id值,說明生成該版本的事務在當前事務生成ReadView後纔開啓,因此該版本不能夠被讀出來。
  • 若是生成被訪問版本的trx_id在min_trx_id和max_trx_id之間,那就須要判斷下trx_id在不在m_ids中:若是在,說明建立ReadView的時候,生成該版本的事務仍是活躍的(沒有被提交),該版本不能夠被讀出來;若是不在,說明建立ReadView的時候,生成該版本的事務已經被提交了,該版本能夠被讀出來。

若是某個數據的最新版本不能夠被讀出來,就順着roll_pointer找到該數據的上一個版本,繼續作如上的判斷,以此類推,若是第一個版本也不可見的話,表明該數據對當前事務徹底不可見,查詢結果就不包含這條記錄了。

看完上面的描述,是否是以爲「雲裏霧裏」,「不知所云」,甚至「腦闊疼,整我的都很差了」。

咱們換個方法來解釋,看會不會更容易理解點:

image.png
在事務啓動的一瞬間(執行CURD操做),會建立出ReadView,對於一個數據版本的trx_id來講,有如下三種狀況:

  • 若是落在低水位,表示生成這個版本的事務已經提交了,或者是當前事務本身生成的,這個版本可見。
  • 若是落在高水位,表示生成這個版本的事務是將來才建立的,這個版本不可見。
  • 若是落在中間水位,包含兩種狀況: a. 若是當前版本的trx_id在活躍事務列表中,表明這個版本是由尚未提交的事務生成的,這個版本不可見; b. 若是當前版本的trx_id不在活躍事務列表中,表明這個版本是由已經提交的事務生成的,這個版本可見。

上面我比較簡單的解釋了下ReadView,用了兩種方式來講明如何判斷當前數據版本是否可見,不知道各位看官是否是有了一個比較模糊的概念,有了ReadView的基本概念,咱們就能夠具體看下READ COMMITTED、REPEATABLE READ這兩個事務隔離級別爲何讀到的數據是不一樣的,以及上述規則是如何應用的。

READ COMMITTED——每次讀取數據都會建立ReadView

假設,如今系統只有一個活躍的事務T,事務id是100,事務中修改了數據,可是尚未提交,造成的版本鏈是這樣的:

image.png

如今A事務啓動,而且執行了select語句,此時會建立出一個ReadView,m_ids是【100】,min_trx_id是100, max_trx_id是101,creator_trx_id是0。

爲何m_ids只有一個,爲何creator_trx_id是0?這裏再次強調下,只有在事務中執行insert/update/delete語句才能得到事務id。

那麼A事務執行的select語句會讀到什麼數據呢?

  1. 判斷最新的數據版本,name是「夢境地底王」,對應的trx_id是100,trx_id在m_ids裏面,說明當前事務是活躍事務,這個數據版本是由尚未提交的事務建立的,因此這個版本不可見。
  2. 順着roll_pointer找到這個數據的上一個版本,name是「地底王」,對應的trx_id是99,而ReadView中的min_trx_id是100,trx_id<min_trx_id,表明當前數據版本是由已經提交的事務建立的,該版本可見。

因此讀到的數據的name是「地底王」。

咱們把事務T提交了,事務A再次執行select語句,此時,事務A再次建立出ReadView,m_ids是【】,min_trx_id是0, max_trx_id是101,creator_trx_id是0。

由於事務T已經提交了,因此沒有活躍的事務。

那麼事務A第二次執行select語句又會讀到什麼數據呢?

  1. 判斷最新的數據版本,name是「夢境地底王」,對應的trx_id是100,不在m_ids裏面,說明這個數據版本是由已經提交的事務建立的,該版本可見。

因此讀到的數據的name是「夢境地底王」。

REPEATABLE READ ——首次讀取數據會建立ReadView

假設,如今系統只有一個活躍的事務T,事務id是100,事務中修改了數據,可是尚未提交,造成的版本鏈是這樣的:

image.png

如今A事務啓動,而且執行了select語句,此時會建立出一個ReadView,m_ids是【100】,min_trx_id是100, max_trx_id是101,creator_trx_id是0。

那麼A事務執行的select語句會讀到什麼數據呢?

  1. 判斷最新的數據版本,name是「夢境地底王」,對應的trx_id是100,trx_id在m_ids裏面,說明當前事務是活躍事務,這個數據版本是由尚未提交的事務建立的,因此這個版本不可見。
  2. 順着roll_ponit找到這個數據的上一個版本,name是「地底王」,對應的trx_id是99,而ReadView中的min_trx_id是100,trx_id<min_trx_id,表明當前數據版本是由已經提交的事務建立的,該版本可見。

因此讀到的數據的name是「地底王」。

細心的你,必定發現了,這裏我就是複製粘貼,由於在REPEATABLE READ事務隔離級別下,事務A首次執行select語句建立出來的ReadView和在READ COMMITTED事務隔離級別下,事務A首次執行select語句建立出來的ReadView是同樣的,因此判斷流程也是同樣的,因此我就偷懶了,copy走起。

隨後,事務T提交了事務,因爲REPEATABLE READ是首次讀取數據纔會建立ReadView,因此事務A再次執行select語句,不會再建立ReadView,用的仍是上一次的ReadView,因此判斷流程和上面也是同樣的,因此讀到的name仍是「地底王」。

本篇博客到這裏就結束了。

相關文章
相關標籤/搜索