本篇博客參考掘金小冊——MySQL 是怎樣運行的:從根兒上理解 MySQL數據庫
雖然咱們不是DBA,可能對數據庫沒那麼瞭解,可是對於數據庫中的索引、事務、鎖,咱們仍是必需要有一個較爲淺顯的認識,今天我就和你們聊聊事務。bash
說到事務,不得不提到轉帳的事情,幾乎全部的關於事務的文章都會提到這個老掉牙的案例,我也不例外。服務器
轉帳在數據庫層面能夠簡單的抽象成兩個部分:session
若是先從本身的帳戶中扣除轉帳金額,再往對方帳戶中增長轉帳金額,扣除執行成功,增長執行失敗,那本身的帳戶白白少了100塊,欲哭無淚。架構
若是先往對方帳戶中增長轉帳金額,再從本身的帳戶中扣除轉帳金額,增長執行成功,扣除執行失敗,那對方帳戶白白增長了100塊,本身的帳戶也沒有扣錢,喜大普奔。併發
不論是讓你欲哭無淚,仍是喜大普奔,銀行都不會容忍這樣的事情發生,他們會引入事務來解決這類問題。性能
四種特性,簡稱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;
複製代碼
若是sessionB在回滾事務的時候把sessionA的修改也給回滾了,致使sessionA的提交丟失了,這種現象就被稱爲「髒寫」。sessionA會一臉懵逼,我明明修改了數據,也提交了數據,爲何數據沒有變化呢。
咱們知道了在併發執行事務的時候,會遇到什麼問題,有些問題比較嚴重,有些問題比較輕微,通常來講,咱們認爲按照嚴重性排序是這樣的:
髒寫>髒讀>不可重複讀>幻讀
在SQL標準定義中,設定了四種隔離級別,來解決上述的問題:
由於髒寫的問題實在太嚴重了,在任何隔離級別下,都不會有髒寫的問題。
前面說的都是開胃菜,相信大部分小夥伴對於上述內容都是手到擒來,因此我連如何修改事務隔離級別都沒有介紹,各類實驗也都沒有作,就是要把大量的時間、文字投入到這一部份內容中來。
MVCC,全稱是Mutil-Version Concurrency Control,翻譯成中文是多版本併發控制,MySQL就利用了MVCC來判斷在一個事務中,哪一個數據能夠被讀出來,哪一個數據不能被讀出來。
在看MVCC以前,咱們有必要知道另一個知識點,數據庫存儲一行行數據,是分爲兩個部分來存儲的,一個是數據行的額外信息(本篇博客不涉及),一個是真實的數據記錄,MySQL會爲每一行真實數據記錄添加兩三個隱藏的字段:
以下圖所示:
在這裏須要着重說明下事務id,當咱們開啓一個事務,並不會立刻得到事務id,哪怕咱們在事務中執行select語句,也是沒有事務id的(事務id爲0),只有執行insert/update/delete語句才能得到事務id,這一點尤其重要。
其中和MVCC緊密相關的是transaction_id和roll_pointer兩個字段,在開發過程當中,咱們無需關心,可是要研究MVCC,咱們必須關心。
若是有相似這樣的一行數據:
表明這行數據是由transaction_id爲9的事務建立出來的,roll_pointer是空的,由於這是一條新紀錄。實際上,roll_pointer並非空的,若是真要解釋,須要繞一大圈,理解成空的,問題也不大。
當咱們開啓事務,對這條數據進行修改,會變成這樣:
有點感受了吧,這就像一個單向鏈表,稱之爲「版本鏈」,最上面的數據是這個數據的最新版本,roll_pointer指向這個數據的舊版本,給人的感受就是一行數據有多個版本,是否是符合「多版本併發控制」中的「多版本」這個概念, 那麼「併發控制」又是怎麼作到的呢,別急,繼續往下看。
哎,下面又要引出一個新的概念:ReadView。
對於READ UNCOMMITTED來講,能夠讀取到其餘事務尚未提交的數據,因此直接把這個數據的最新版本讀出來就能夠了,對於SERIALIZABLE來講,是用加鎖的方式來訪問記錄。
剩下的就是READ COMMITTED和REPEATABLE READ,這兩個事務隔離級別都要保證讀到的數據是其餘事務已經提交的,也就是不能無腦把一行數據的最新版本給讀出來了,可是這兩個仍是有必定的區別,最核心的問題就在於「我到底能夠讀取這個數據的哪一個版本」。
爲了解決這個問題,ReadView的概念就出現了,ReadView包含四個比較重要的內容:
有了這個ReadView,只要按照下面的判斷方式就能夠解決「我到底能夠讀取這個數據的哪一個版本」這個千古難題了:
若是某個數據的最新版本不能夠被讀出來,就順着roll_pointer找到該數據的上一個版本,繼續作如上的判斷,以此類推,若是第一個版本也不可見的話,表明該數據對當前事務徹底不可見,查詢結果就不包含這條記錄了。
看完上面的描述,是否是以爲「雲裏霧裏」,「不知所云」,甚至「腦闊疼,整我的都很差了」。
咱們換個方法來解釋,看會不會更容易理解點:
在事務啓動的一瞬間(執行CURD操做),會建立出ReadView,對於一個數據版本的trx_id來講,有如下三種狀況:上面我比較簡單的解釋了下ReadView,用了兩種方式來講明如何判斷當前數據版本是否可見,不知道各位看官是否是有了一個比較模糊的概念,有了ReadView的基本概念,咱們就能夠具體看下READ COMMITTED、REPEATABLE READ這兩個事務隔離級別爲何讀到的數據是不一樣的,以及上述規則是如何應用的。
假設,如今系統只有一個活躍的事務T,事務id是100,事務中修改了數據,可是尚未提交,造成的版本鏈是這樣的:
如今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語句會讀到什麼數據呢?
因此讀到的數據的name是「地底王」。
咱們把事務T提交了,事務A再次執行select語句,此時,事務A再次建立出ReadView,m_ids是【】,min_trx_id是0, max_trx_id是101,creator_trx_id是0。
由於事務T已經提交了,因此沒有活躍的事務。
那麼事務A第二次執行select語句又會讀到什麼數據呢?
因此讀到的數據的name是「夢境地底王」。
假設,如今系統只有一個活躍的事務T,事務id是100,事務中修改了數據,可是尚未提交,造成的版本鏈是這樣的:
如今A事務啓動,而且執行了select語句,此時會建立出一個ReadView,m_ids是【100】,min_trx_id是100, max_trx_id是101,creator_trx_id是0。
那麼A事務執行的select語句會讀到什麼數據呢?
因此讀到的數據的name是「地底王」。
細心的你,必定發現了,這裏我就是複製粘貼,由於在REPEATABLE READ事務隔離級別下,事務A首次執行select語句建立出來的ReadView和在READ COMMITTED事務隔離級別下,事務A首次執行select語句建立出來的ReadView是同樣的,因此判斷流程也是同樣的,因此我就偷懶了,copy走起。
隨後,事務T提交了事務,因爲REPEATABLE READ是首次讀取數據纔會建立ReadView,因此事務A再次執行select語句,不會再建立ReadView,用的仍是上一次的ReadView,因此判斷流程和上面也是同樣的,因此讀到的name仍是「地底王」。
本篇博客到這裏就結束了。