現有一個交易系統,每次交易都會更新餘額。出帳扣減餘額,入帳增長餘額。爲了保證資金安全,餘額發生扣減時,須要比較現有餘額與扣減金額大小,若扣減金額大於現有餘額,扣減餘額不足,扣減失敗。html
餘額表(省去其餘字段)結構以下:mysql
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
因爲存在併發更新餘額的狀況,在 t3 時刻,使用寫鎖鎖住該行記錄。這樣就能保證事務執行期間不會有其餘事務提交變動。數據庫
如今咱們假設有兩個事務正在發執行該語序,執行順序如圖所示。數組
假設 id=1 記錄 balance=1000,事務隔離等級爲 RR。小夥伴們能夠根據這個執行時序能夠先思考下 t3,t5,t6,t7 結果。安全
注: 以上時序,順序執行。可是事務 1 執行到 t3 時刻,t4 時刻,事務 2 執行時將會被阻塞,後續沒法執行。
t4 時刻以後,只能先將 事務 1 語序執行,事務提交完成後,才能執行 事務 2 剩餘語句。併發
下面放出問題的答案。mvc
t3 (1,1000)高併發
t5 (1,1000)3d
t4 (1,900)
t6 (1,1000)
有沒有跟你結果的不太同樣?
事務 1 查詢結果基本沒什麼問題,事務 2 同一個事務內查詢結果卻不一樣。
如今咱們先帶着疑問,看完下面 MySQL 的相關原理,你就會明白一切。
假設在 RR 下,下圖 id=1 balance=1000 。
上圖時序順序能夠執行
事務 1 將 id=1 記錄 balance 更新爲 900。而後 t5 查詢結果確定仍是 id=1 balance=1000,否則就讀取到髒數據,不符合當前事務隔離級別。
從上面例子能夠看到 id=1 的記錄存在兩個版本,一個爲 balance=1000 ,一個爲 balance=900。
MySQL 使用 MVCC 實現該功能。
MVCC:Multiversion concurrency control,多版本併發控制。摘錄一段淘寶數據庫月報的解釋:
多版本控制: 指的是一種提升併發的技術。最先的數據庫系統,只有讀讀之間能夠併發,讀寫,寫讀,寫寫都要阻塞。引入多版本以後,只有寫寫之間相互阻塞,其餘三種操做均可以並行,這樣大幅度提升了InnoDB的併發度。在內部實現中,與Postgres在數據行上實現多版本不一樣,InnoDB是在undolog中實現的,經過undolog能夠找回數據的歷史版本。找回的數據歷史版本能夠提供給用戶讀(按照隔離級別的定義,有些讀請求只能看到比較老的數據版本),也能夠在回滾的時候覆蓋數據頁上的數據。在InnoDB內部中,會記錄一個全局的活躍讀寫事務數組,其主要用來判斷事務的可見性。
能夠看到 MVCC 主要用來提升併發,還能夠用來讀取老版本數據。下面介紹 MVCC 實現的原理。
首先咱們先看下 MySQL 記錄結構。
能夠看到 MySQL 行記錄除了真實數據之外,還會存在三個隱藏字段,用來記錄額外信息。
DB_TRX_ID:事務id。
DB_ROLL_PTR: 回滾指針,指向 undolog。
ROW_ID:行 id,與這次無關。
具體行記錄結構,能夠參考掘金的小冊『 MySQL 是怎樣運行的:從根兒上理解 MySQL』,說實話小冊寫的真的很好,收益頗豐。哈哈。
MySQL 經過 DB_ROLL_PTR 找到 undolog,而 undolog 記錄數據的變動。這樣 MySQL 就能推導出變動以前記錄內容。
查找過程以下:
若須要知道 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,t1,t2 時刻兩個事務因爲查詢語句,分別創建了一致性視圖。
t3 時刻,因爲事務 1 使用 select.. for update
爲 id=1 這一行上了一把寫鎖,而後獲取到最新結果。而 t4 時刻,因爲該行已被上鎖,事務 2 必須等待事務 1 釋放鎖才能繼續。
t5 時刻根據一致性視圖,不能讀取到其餘事務提交的版本,因此數據沒變。t7 時刻餘額扣減 100,t8 時刻提交事務。
此時最新版本記錄爲 id=1 balance=900。
因爲事務 1 事務提交,行鎖被釋放,t4 獲取到寫鎖。因爲 t4 是當前讀,因此查詢的結果爲最新版本數據(1,900)。
重點來了。t6 查詢時,id=1 這條記錄最新版本數據爲 (1,900)。可是最新版本事務 id,屬於事務 2建立以後未提交的事務,位於活躍事務數組中。因此最新記錄版本對於事務2 是不可見的。沒辦法只能根據 undolog 去讀取上一版本記錄 (1,1000) 。這個版本記錄恰好對於事務 2 可見。
若當前事務隔離級別修改爲 RC,那麼結果就與 RR 不一樣。各位讀者自行分析一下。
下面貼一下 RC 答案。
mysql mvcc
淘寶月報
innodb 相關實現
consistent-read
極客時間- MySQL 專欄--事務究竟是隔離的仍是不隔離的