面試官:MYSQL事務隔離與MVCC多版本併發控制知道嗎?

數據庫的事務隔離

前段時間,公司內部遇到了一個問題,就是咱們建立的同一批任務,別分配給了不一樣的實例去執行,致使線上的結果出現問題。面試

另外一個組的leader說沒有開啓事務,設置下事務就能夠。sql

數據庫一出現一致性問題,就說開啓事務,我就有點疑惑,數據庫的事務究竟是怎麼保證一致性的。數據庫

在看下面的內容,咱們能夠先思考幾個問題。數組

數據庫的隔離級別都有什麼?

數據庫的MVVC視圖是怎麼實現的?session

數據庫的隔離級別是爲了解決什麼問題的?併發

看完上面三個問題,本身能回答上來幾個呢?不急。咱們繼續往下看cors

數據庫的事務

數據庫的事務咱們簡單來講就是用來保證數據的正確性,它只有兩個操做:事務要麼成功,要麼失敗並進行回滾。spa

爲何這麼作呢?這是由於通常咱們進行事務操做,都會進行一組操做。好比你常見的金融轉帳。日誌

在這個轉帳事務裏面包含2個操做:code

  • 扣本身銀行帳戶的錢
  • 給對應的帳戶添加收到的錢。

如今思考下,若是咱們沒有添加事務,那麼會出現什麼樣的狀況呢?

  1. 若是先扣錢成功,執行給別人加錢失敗。而錢已經扣了,對方沒收到錢,你說怎麼辦?
  2. 若是先給對方加錢,而扣你錢的時候沒扣成功。這錢銀行給的補助嗎?嘿嘿,那銀行確定不開心。

因此了咱們只能在這種操做中使用事務,來保證執行的成功與失敗,失敗了要進行回滾,保證扣錢的操做也不執行。

事務的ACID

事務具備四個特性,這四個特性簡稱爲ACID

  • 原子性Atomicity:同一組操做,要麼作,要麼不作,一組中的一兩個執行成功不表明成功,全部成功才能夠。這就是原子性,作或者不作(失敗進行回滾)。
  • 一致性Consistency:數據的一致性,就像上面的舉例說的,你扣錢了,對方沒加錢,那確定不行。
  • 隔離性Isolation:多個數據庫操做同一條數據時,不能互相影響。不能你這邊變更,那邊數據空間就變換了。
  • 持續性Durability: 事務結果提交後,變更就是永久性的,接下來的操做或者系統故障不能讓這個記錄丟失。

今天主要說的事務就是隔離。看看事務是怎麼保證數據之間的隔離

事務的隔離級別

不一樣的事務隔離級別對應的不一樣的數據執行效率,隔離的越嚴格,那麼執行的效率就約低下,下面的四個隔離級別是原來越嚴格。

  • 讀未提交(read uncommitted):指數據在事務執行時,尚未提交,其餘事務就能夠看到結果
  • 讀提交(read committed):指數據在其事務提交後,其餘事務才能看到結果。視圖是在執行sql語句的時候進行建立,具體視圖看下面的數據隔離是怎麼實現的
  • 可重複讀(repeatable read):一個事務在執行過程當中,看到的結果與其啓動的時候看到的內容是一致的。啓動的時候會建立一個視圖快照,該事務狀態下,會看的一致是這個視圖快照內容,其餘事務變動是看不到的。注意是讀取的過程,若是是更新,那麼會採用當前讀,就是其餘事務的更新操做會拿到結果,用來保證數據一致性
  • 串行化(serializable):顧名思義,就是將多個事務進行串行化(讀寫過程當中加鎖,讀寫衝突,讀讀衝突),一個事務結束後,另一個事務才能執行,帶來的效果就是並行化很差,效率低下。

Mysql中默認的事務隔離級別是可重複讀,使用下面這個命令進行查看當前的事務級別,

show variables like 'transaction_isolation';

# 下面的語句進行修改事務的級別。

SET session TRANSACTION ISOLATION LEVEL Serializable;(參數能夠爲:Read uncommitted,Read committed,Repeatable,Serializable)

事務級別

事務的啓動方式

在程序中,咱們不少時候都是默認的自動提交,也就是一個sql操做就是一條事務,但有時候須要的是多個SQL進行組合,咱們就要顯式的開啓事務。

顯示開啓的語句是用 begin或者 start transaction.一樣在事務結束的時候使用commit進行提交,失敗使用rollbakc進行回滾。

固然若是不想讓SQL進行自動提交,咱們就將自動提交進行關閉set autocommit=0,這樣事務就不會自動提交,須要咱們手動的執行commit.

如何避免長事務

關閉自動提交事務後,就須要咱們來本身提交事務,這時候每一個語句執行都是這樣的。

begin

sql語句

commit

若是咱們在程序編寫中,原本一個sql解決的操做,結果忘記進行事務的提交,到下下下一個SQL才進行commit,這樣就會出現長事務。

而長事務每每會形成大量的堵塞與鎖超時的現象,事務中若是有讀寫(讀讀不衝突,讀寫衝突,寫寫衝突)操做,那麼會將數據進行鎖住,其餘事務也要進行等待。

因此在程序中,咱們應該儘可能避免使用大事務,一樣也避免咱們寫程序的時候出現偶然的大事務(失誤😁)。

解決辦法是咱們將自動提交打開,當須要使用事務的時候纔會顯示的開啓事務

程序中出現大量的事務等待怎麼辦

在MySQL中想定位一個長事務問題仍是很方便的。

首先咱們先找到正在執行的長事務是什麼。

select t.*,to_seconds(now())-to_seconds(t.trx_started) idle_time from INFORMATION_SCHEMA.INNODB_TRX t G

該語句會展現出事務的執行的開始時間,咱們能夠很簡單的算出,當前事務執行了多久,其中上面的idle_time就是執行的事務時間

假設咱們如今設定的超過30s執行的事務都是長事務,可使用下面語句進行過濾30s以上的長事務。

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>30

經過information_schema.innodb_trx 咱們就能定位到長事務。

從而決定長事務是否要被殺死,仍是繼續等待。若是出現死鎖的狀況,處理方式也是相似,查找到死鎖的語句,而後進行殺死某個語句的執行(有點暴力)。

數據隔離是怎麼實現的

注意:本次數據隔離是創建在可重複讀的場景下

在可重複讀的場景下,咱們瞭解每次啓動事務的時候,會在當前啓動一個視圖,而這個視圖是整個數據庫的視圖快照。

嘿嘿,是否是想數據庫那麼大,爲啥咱們沒有感受到建立快照時間的消耗呢?

這是由於數據庫建立的視圖快照利用了全部數據都有多個版本的特性,來實現快速建立視圖快照的能力

那數據多個版本是怎麼回事呢?

準備下數據

先別急,咱們準備下數據。

如今建立一個表,而且插入三條數據。

create table scores

(

id int not null

primary key,

score float null

);

INSERT INTO scores (id, score) VALUES (1, 3.5);

INSERT INTO scores (id, score) VALUES (2, 3.65);

INSERT INTO scores (id, Score) VALUES (3, 4);

在開始使用前咱們要了解兩個小知識點。begin/start transaction 與 start transaction with consistent snapshot。

  • begin/start transaction 視圖的建立是創建在begin/ start transaction 以後SQL語句纔會建立視圖, 好比 下面案例
begin

select source from scores; //視圖是在這裏開始建立 而不是在begin那裏建立

commit
  • start transaction with consistent snapshot:則是該語句執行後,就建立視圖。

瞭解上面兩個建立事務的區別後,咱們來看下視圖是怎麼建立出來多個數據版本的. 如下SQL在兩個窗口打開。

事務A | 事務B | 結果

start transaction with consistent snapshot | 開啓事務,並建立視圖 |

--| start transaction with consistent snapshot |開啓事務,並建立視圖

select score from scors where id =2 | -- | 事務A中的值爲3.65

-- | update scores set scores = 10 where id =2 | 事務B修改成10

--| select score from scores where id =2 | 事務B顯示爲10

select score from scores where id =2 | --| 事務A顯示爲3.65

select score from scores where id =2 for update | --| 會被鎖住,等待事務B釋放鎖(間隙鎖)

-- |commit | 提交事務B

select score from scores where id =2 for update | --| 這個語句能夠看到變成了10(利用了當前讀)

select score from scores where id =2 | --| 不加 for update 那麼結果仍是3.65

commit|---|---| 提交A的結果

上述流程就是兩個不一樣的請求過來,對數據庫同一個表的不一樣操做。

當事務A執行start transaction with consistent snapshot以後,A的視圖就開始被建立了,這時候是看不到事務B對其中的修改,就算事務Bcommit以後,只要事務A不結束,它看到的結果就是它啓動時刻的值。

這就與不重複提交,執行過程當中看到的結果與啓動的時候看到的結果是一致的這句話對應上了

快照多版本

前面說了,快照是事務的啓動的時候是基於整個數據庫的,而整個數據庫是很大,那MYSQL是怎麼讓咱們無感並快速建立一個快照呢。

快照多版本你能夠認爲是由如下兩部分構成。

  • 事務id(transaction id):這個是由事務啓動的時候向InnoDB啓動時申請的。而且必定注意哦它是遞增的。
  • row trx_id:這個id其實就是事務ID,每次事務更新數據的時候回將事務ID賦值給這個數據版本的事務ID上,將這個數據版本的事務ID稱爲 row trx_id.

當一行記錄存在多個數據版本的時候,那麼就有多個row trx_id 。舉個例子

版本 | 值|事務ID| 對應的語句操做

v1 | score =3 | 89| --

v2 | score =5| 90| update scores set score = 5 where id =3; select score from scores where id =3;|

v3 | score = 6 | 91 | update scores set score = 6 where id =3;|

v1->v2->v3 這裏面涉及了三個版本的迭代。中間是經過undo log 日誌來保存更新的記錄的。

注意啓動快照以後,可重複讀隔離狀況下,獲取到v1的值,不是說MYSQL直接存儲的該值,而是利用如今這條記錄的最新版本與undo log日誌計算出來的,好比經過v3 ->v2—>v1 計算出v1中score值。

版本圖

版本計算

上面簡單說了下版本的計算規則,可是在MYSQL中,版本並非那麼簡單的計算的,咱們如今來看下到底怎麼計算的,

這個兩點咱們在注意一下:

  • 事務在啓動的時候會向InnoDB的事務系統申請事務ID,這個事務ID是嚴格遞增的。
  • 每行數據是多個版本,這個版本的id就是row trx_id,而事務更新數據(更新數據的時候纔會生成一個新的版本)的時候會生成一個新的數據版本,並把事務ID賦值給這個數據的事務ID==row trx_id,
  1. 事務啓動的時候,能看到全部已經提交事務的結果,可是他啓動以後,其餘事務的變動是看不到的。
  2. 當事務啓動的瞬間,除了已經提交的事務,建立的瞬間還會存在正在運行的事務,MYSQL是把這些正在運行的事務ID放入到一個數組中。數組中最小的事務ID記爲低水位,當前系統中建立過的事務ID最大值+1記爲高水位。
舉個簡單的例子。

a. 注意一點:獲取事務ID與建立數組不是一個原子操做,因此存在事務id爲8,而後又存在當前MYSQL中存在活躍事務ID爲9 10的事務。

b. 事務ID低於低水位那麼對於當前事務確定是可見的,事務ID高於高水位的事務ID值,則對當前事務不可見.

c. 事務ID 位於低水位與高水位之間分爲兩種狀況。

  • 若是事務id是在活躍的數組中表示這個版本是正在執行,可是結果尚未提交,因此這些事務的變動是不會讓固然事務看到的。
  • 事務id若是沒有在活躍數組中,表明這個事務是已經提交了,因此可見。好比如今建立了90,91,92三個事務,91執行的比較快,提交完畢,90和92尚未提交.這時候建立了一個新的事務id爲93,那麼在活躍的數組中的事務就是90,92,93,你看91是已經提交了,它的事務還在這個低水位與高水位之間,但結果對於93是可見。

總的上面來講就是你在我建立的時候事務結果已經提交,那麼是可見的,以後提交那麼就是不可見的。

讀取流程

上面簡單說了下老版本視圖中的數據是經過最新的版本與undo log 計算出來的,那到底怎麼就算的呢?

事務A | 事務B | 結果

start transaction with consistent snapshot 事務 id 89| 開啓事務,並建立視圖 |

--| start transaction with consistent snapshot 事務id 92 |開啓事務,並建立視圖

select score from scors where id =2 | -- | 事務A中的值爲3.65

-- | update scores set scores = 10 where id =2 | 事務B修改成10

--| select score from scores where id =2 | 事務B顯示爲10

select score from scores where id =2 | --| 事務A顯示爲3.65

commit|---|---| 提交A的結果

仍是看這個事務操做。

下面是數據變更的流程。

  • 假設開始以前有兩個活躍的事務ID爲 78,88.
  • 事務A啓動的時候會將78 88,包含它本身放入到活躍數組中。
  • 事務A 操做的語句select score from scors where id =2將其看到的結果認爲是v1版本數據好比其如今row trx_id(注意:row trx_id是數據行被更新後事務id纔會賦值給row trx id上)是86,而且保存好。
  • 事務B啓動時,會發如今活躍數組是78,88,89,本身的92.
  • 事務B 執行更新語句語句後,會生成一個新的版本V2,數據變換就是V1-->V2。記錄中間變化的是undo log日誌。這樣ID 89存儲的數據就變成了歷史數據。數據版本row trx_id則是92
  • 事務A 查詢score數據,就會經過先查到如今的V2版本視圖,找到對應的row trx_id = 92,發現row trx_id 位於高水位上,則拋棄這個值,經過V2找到V1,row trx_id爲86,而86大於低水位,而低於高水位89+1.可是因爲86沒有在活躍數組中,並且屬於已經提交的事務,則當前事務是能看到該結果的,因此事務A能拿到讀取的值。

你看通過簡單的幾步,咱們就拿到了想要讀取的事務數據,因此不論事務A何時查詢,它拿到的結果都是跟它讀取的數據是一致的。

你看有了MVCC(多版本併發控制)計算別的事務更改了值也不會影響到當前事務讀取結果的過程。

咱們常常說不要寫一個長事務,經過上面的讀取流程能夠看到,長事務存在時間長的話,數據版本就會有不少,那麼undo log日誌就須要保存很久,這些回滾日誌會佔用大量的內存存儲空間。

當沒有事務須要讀取該日誌與版本數據的時候,這個日誌才能夠刪除,從而釋放內存空間。

更新流程

事務A | 事務B | 結果

start transaction with consistent snapshot 事務 id 89| 開啓事務,並建立視圖 |

--| start transaction with consistent snapshot 事務id 92 |開啓事務,並建立視圖

select score from scors where id =2 | -- | 事務A中的值爲3.65

-- | update scores set scores = 10 where id =2 | 事務B修改成10

--| select score from scores where id =2 | 事務B顯示爲10

select score from scores where id =2 | --| 事務A顯示爲3.65

select score from scores where id =2 for update | --| 會被鎖住,等待事務B釋放鎖(間隙鎖)

-- |commit | 提交事務B

select score from scores where id =2 for update | --| 這個語句能夠看到變成了10(利用了當前讀)

select score from scores where id =2 | --| 不加 for update 那麼結果仍是3.65

commit|---|---| 提交A的結果

上面說了讀取的過程,其實在事務中,咱們還有更新流程,更新流程比較簡單,更新過程咱們須要保證數據的一致性,不能說別人修改了,咱們還看不到,那樣就會形成數據的不一致。

爲了保證看到最新的數據,會對更新行的操做加鎖(行鎖),加鎖以後,其餘事務對行進行更新操做,必須等待其餘事務commit以後才能獲取到最新的值,這個過程被稱爲當前讀

想要讀取過程當中得到最新的值可使用 上面的語句select score from scores where id =2 for update ,就能夠看到當前最新值。

總結

本小節主要梳理了事務的隔離級別,事務的MVCC多版本併發控制實現原理。

事務在面試中是比較多的一個點,這樣的題目能夠多種變換,咱們剛開始題目提到的三個問題已經能夠解答了。

你來嘗試回答下?

下期會說下數據庫中的幻讀,幻讀也是面試中常常遇到的問題哦。

相關文章
相關標籤/搜索