哎,最近有點忙,備考複習不利,明天還要搬家,好難啊!!html
本想着這周鴿了,可是想一想仍是不行,爬起來,更新一下,周更可不能斷。偷懶一下,修改一下以前的一篇歷史文章,從新發布一下。mysql
ps: 發這篇文章的時候,正在打加賽,JD 加油!!sql
這是一個真實的生產事件,事件原由以下:數據庫
現有一個交易系統,每次產生交易都會更新相應帳戶的餘額,出帳扣減餘額,入帳增長餘額。數組
爲了保證資金安全,餘額發生扣減時,須要比較現有餘額與扣減金額大小,若扣減金額大於現有餘額,扣減餘額不足,扣減失敗。安全
帳戶表(省去其餘字段)結構以下:併發
CREATE TABLE `account` ( `id` bigint(20) NOT NULL, `balance` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin;
扣減餘額時,sql 語序以下所示:ide
ps:看到上面的語序,有沒有個小問號?爲何相同查詢了這麼屢次?高併發
其實這些 SQL 語序並不在同個方法內,而且有些方法被抽出複用,因此致使一些相同查詢結果沒辦法往下傳遞,因此只得再次從數據庫中查詢。學習
爲了防止併發更新餘額,在 t3 時刻,使用寫鎖鎖住該行記錄。若加鎖成功,其餘線程的若也執行到 t3,將會被阻塞,直到前一個線程事務提交。
t5 時刻,進入到下一個方法,再次獲取帳戶餘額,而後在 Java 方法內比較餘額與扣減金額,若餘額充足,在 t7 時刻執行更新操做。
上面的 SQL 語序看起來沒有什麼問題吧,實際也是這樣的,帳戶系統已經在生產運行好久,沒出現什麼問題。可是這裏須要說一個前提,系統數據庫是 Oracle 。
可是從上面表結構,能夠得知這次數據庫被切換成 MySQL,系統其餘任何代碼以及配置都不修改(sql 存在小改動)。
就是這種狀況下,併發執行發生餘額多扣,即實際餘額明明小於扣減金額,可是卻作了餘額更新操做,最後致使餘額變成了負數。
下面咱們來重現併發這種狀況,假設有兩個事務正在發執行該語序,執行順序如圖所示。
注意點:數據庫使用的是 MySQL,默認事務隔離等級,即 RR。數據庫記錄爲 id=1 balance=1000,假設只有當時只有這兩個事務在執行。
各位讀者能夠先思考一下,t2,t3,t4,t5,t6,t11 時刻餘額多少。
下面貼一下事務隔離等級RR 下的答案。
事務 1 的查詢結果爲:
事務 2 的查詢結果爲:
有沒有跟你想的結果的同樣?
接着將事務隔離等級修改爲 RC,一樣再來思考一下 t2,t3,t4,t5,t6,t11 時刻餘額。
再次貼下事務隔離等級RC 下的答案。
事務 1 的查詢結果爲:
事務 2 的查詢結果爲:
事務 1 的查詢結果,你們應該會沒有什麼問題,主要疑問點應該在於事務 2,爲何換了事務隔離等級結果卻不太同樣?
下面咱們先帶着疑問,瞭解一下 MySQL 的相關原理 ,看完你就會明白這一切。
咱們先來看下一個簡單的例子,
事務隔離等級爲 RR , id=1 balance=1000
更新時序
事務 1 將 id=1 記錄 balance 更新爲 900,接着事務 2 在 t5 時刻查詢該行記錄結果,很顯然該行記錄應該爲 id=1 balance=1000。
若是 t5 查詢最新結果 id=1 balance=900,這就讀取到事務 1 未提交的數據,顯然不符合當前事務隔離級別。
從上面例子能夠看到 id=1 的記錄存在兩個版本,事務 1 版本記錄爲 balance=1000 ,事務 2 版本記錄爲 balance=900。
上述功能,MySQL 使用 MVCC 機制實現功能。
MVCC:Multiversion concurrency control,多版本併發控制。摘錄一段淘寶數據庫月報的解釋:
多版本控制: 指的是一種提升併發的技術。最先的數據庫系統,只有讀讀之間能夠併發,讀寫,寫讀,寫寫都要阻塞。引入多版本以後,只有寫寫之間相互阻塞,其餘三種操做均可以並行,這樣大幅度提升了 InnoDB 的併發度。在內部實現中,與 Postgres 在數據行上實現多版本不一樣,InnoDB 是在 undolog 中實現的,經過 undolog 能夠找回數據的歷史版本。找回的數據歷史版本能夠提供給用戶讀(按照隔離級別的定義,有些讀請求只能看到比較老的數據版本),也能夠在回滾的時候覆蓋數據頁上的數據。在 InnoDB 內部中,會記錄一個全局的活躍讀寫事務數組,其主要用來判斷事務的可見性。
能夠看到 MVCC 主要用來提升併發,還能夠用來讀取老版本數據。
在學習 MVCC 原理以前,首先咱們須要瞭解 MySQL 記錄結構。
行記錄
如上圖所示,account 表一行記錄,除了真實數據以外,還會存在三個隱藏字段,用來記錄額外信息。
MySQL InnoDB 裏面每一個事務都會有一個惟一事務 ID,它在事務開始的時候會跟 InnoDB 的事務系統申請的,而且嚴格按照順序遞增的。
每次事務更新數據時,將會生成一個新的數據版本,而後會把當前的事務 id 賦值給當前記錄的 DB_TRX_ID。而且數據更新記錄(1,1000---->1,900)將會記錄在 undo log(回滾日誌)中,而後使用當前記錄的 DB_ROLL_PTR 指向 und olog。
這樣 MySQL 就能夠經過 DB_ROLL_PTR 找到 undolog 推導出以前版本記錄內容。
查找過程以下:
若須要知道 V1 版本記錄,首先根據當前版本 V3 的 DB_ROLL_PTR 找到 undolog,而後根據 undolog 內容,計算出上一個版本 V2。以此類推,最終找到 V1 這個版本記錄。
V1,V2 並非物理記錄,沒有真實存在,僅僅具備邏輯意義。
一行數據記錄可能同時存在多個版本,但並非全部記錄都能對當前事務可見。否則上面 t5 就可能查詢到最新的數據。因此查找數據版本時候 MySQL 必須判斷數據版本是否對當前事務可見。
MySQL 會在事務開始後創建一個一致性視圖(並非馬上創建\),在這個視圖中,會保存全部活躍的事務(還未提交的事務\)。
假設當前事務保存活躍事務數組爲以下圖。
判斷版本對於當前事務是否可見時,基於如下規則判斷:
4 這個規則可能比較繞,結合上面圖片比較好理解。
以上判斷規則可能比較抽象,看不懂,沒事,咱們再用大白話解釋一下:
一致性視圖只會在 RR 與 RC 下才會生成,對於 RR 來講,一致性視圖會在第一個查詢語句的時候生成。而對於 RC 來講,每一個查詢語句都會從新生成視圖。
MySQL 使用 MVCC 機制,能夠讀取以前版本數據。這些舊版本記錄不會且也沒法再去修改,就像快照同樣。因此咱們將這種查詢稱爲快照讀。
固然並非全部查詢都是快照讀,select .... for update/ in share mode 這類加鎖查詢只會查詢當前記錄最新版本數據。咱們將這種查詢稱爲當前讀。
講完原理以後,咱們回過頭分析一下上面查詢結果的緣由。
這裏咱們將上面答案再貼過來。
事務隔離級別爲 RR,t2,t3 時刻兩個事務因爲查詢語句,分別創建了一致性視圖。
t4 時刻,因爲事務 1 使用 select.. for update
爲 id=1 這一行上了一把鎖,而後獲取到最新結果。而 t5 時刻,因爲該行已被上鎖,事務 2 必須等待事務 1 釋放鎖才能繼續執行。
t6 時刻根據一致性視圖,不能讀取到其餘事務提交的版本,因此數據沒變。t8 時刻餘額扣減 100,t9 時刻提交事務。
此時最新版本記錄爲 id=1 balance=900。
因爲事務 1 事務已提交,行鎖被釋放,t5 成功獲取到鎖。因爲 t5 是當前讀,因此查詢的結果爲最新版本數據(1,900)。
重點來了,當前這條記錄的最新版本數據爲 (1,900),可是最新版本事務 id,倒是事務 2 建立以後未提交的事務,位於活躍事務數組中。因此最新記錄版本對於事務 2 是不可見的。
沒辦法只能根據 undolog 去讀取上一版本記錄 (1,1000) ,這個版本記錄恰好對於事務 2 可見,因此 t11 的記錄爲 (1,1000)。
而當咱們將事務隔離等級修改爲 RC,每次都會從新生成一致性視圖。因此 t11 時刻從新生成了一致性視圖,這時候事務 1 已提交,當前最新版本的記錄對於事務 2 可見,因此 t11 的結果將會變爲 (1,900)。
MySQL 默認事務隔離等級爲 RR,每一行數據(InnoDB)的均可以有多個版本,而每一個版本都有獨一的事務 id。
MySQL 經過一致性視圖確保數據版本的可見性,相關規則總結以下:
[1] https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html[2] http://mysql.taobao.org/monthly/2017/12/01/[3] http://mysql.taobao.org/monthly/2018/11/04/[4] https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html