注意本文創建在理解mysql的兩種reads和行鎖的基礎上,若是對none-locking reads、gap lock或insert intention這些概念不熟悉請先閱讀《上篇》。mysql
本文主要有如下幾個內容:與locking並駕齊驅的併發處理機制MVCC,sql92對isolation級別的規定以及innodb的實現,innodb不一樣的隔離級別對兩種reads的影響。sql
在上一篇中咱們講到了鎖,鎖是一種在併發場景保證數據一致性的常見手段。此次要講的MVCC能夠說是和鎖同一個層次的設計。MVCC的是multi-version concurrency control的縮寫也就是多版本併發控制,因爲名字太長了你們廣泛稱之爲MVCC。 MVCC本質上是一種經過多版本的機制來管理併發的策略。因爲鎖會限制了效率,因此MVCC經常做爲一種對於鎖的補充方案來管理read與write併發請求之間的一致性問題。從這裏能夠看到併發環境下的一致性管理又多了個MVCC的設計思路。數據庫
基本上全部的主流數據庫都實現了MVCC,各個數據庫對MVCC的實現方案和細節不徹底相同,但總體思路都是基於version的讀寫隔離:一個trx的write操做會致使版本變化,同時保留老的數據版本,供須要老版本數據的其餘trx來讀取。bash
實現MVCC的第一步就是須要一些「工具」來記錄version,最好這些「工具」能幫忙判斷應該讀哪一個版本的數據。在innodb的設計裏,transactionId事務id是記錄verison的核心工具。TransactionId在innodb中是自增的,大的Transaction在小的transaction以後開始(注意這個trxId不能簡單的認爲是自動開始下一個trx的時間,在個Id是和select語句實行時間相關的)。innodb在每一行增長了一個隱藏的字段DB_TRX_ID來表示最後一次嘗試修改該行的Transaction,這個字段叫作DB_TRX_ID。這個字段幫助了select語句來判斷當前的row data是否能夠讀,是否是本身能看到的version版本。併發
那麼歷史版本存在哪兒,這個有兩種設計方向:一種是仍是存在原地,只是打標爲delete,從新建一條目前有效的數據;另外一種方向是原地更改數據,更改數據的版本號也就是trx_id,歷史數據統一放到別處。mvc
針對行數據innodb採用的是第二種方法(postgresql採用的是第一種方案):innodb的每一行上新增了DB_ROLL_PTR,回滾指針,指針指向rollback segment中這一行以前的一個數據版本。rollback segment是 undolog裏的一部分這裏面記錄了歷史版本的數據狀況,之因此稱爲rollback和undo是由於這部分信息的另外一個關鍵做用適用於事務的回滾。工具
innodb把dml分爲兩類,對已有記錄的修改的update和delete,新增數據的操做insert。這種設定和咱們在上篇講到的兩類鎖——針對記錄的鎖和針對空隙的鎖——是一致的。在這種分類思路下undo log分紅了兩部分,update和insert區域。 innodb認爲insert操做在commit以後的rollback信息能夠立刻清除掉,而update類的信息須要保留到全部MVCC支持的一致讀再也不須要這部分信息以後才能進行purge(purge是指innodb後臺線程對undolog中無用數據的一種gc操做)。post
正如剛剛講的原地更新的方案,update(update+delete)和insert語句執行時,彙集索引上的row data會直接修改/新增,修改前的行數據會存到undo log中,並在row的pointer中會更新爲新的undo log的位置。學習
當另外一個trx的select發現這一行的trxId大於本身的trxId時,就會根據指針找到undolog中該行的歷史記錄一直找到最大的小於本身trxId的修改記錄便可。(這是一種trxId知足要求的判斷,在不一樣的隔離級別下這個判斷的方式不一樣)。ui
如何識別一行就須要一個rowId的設計,因爲pk也可能被更新,而自增不變的rowId是識別一行的可靠id。因此行數據上還有一個rowId的隱藏字段。
這樣當須要回滾時只要去找到當前行的歷史版本就能夠回滾, 當別的trx來select時發現rowId對應的trxId與本身trx的快照snaptshot中的該rowId對應的trxId(在一致性環境下trx會保存本身的select快照裏的版本信息)並不一致,也會根據pointer去查undo log來複原歷史版本的數據返回給用戶。從trx快照這個設計其實能讓咱們更好的體會到爲何一個trx稱爲一個unit of work。
剛剛講到的在record上原地更新實際上是基於彙集索引的設計,由於record是存放在彙集索引的葉子節點的。而次級索引的葉子節點記錄只是記錄了彙集索引的id,並無一行數據,因此MVCC針對次級索引的實現方案與上文所述的並不相同。
次級索引上沒有新增字段也沒有原地更新,而是採用上述講到的第一個思路:不在原來數據上作更改,而是生成新的數據,原來的數據entry會被標記成爲deleted,新的index索引所在的page上會記錄更新的trxId。當select trx發現了delete或者page上更大的trxId,select會回表查詢彙集索引,並經過彙集索引的信息去查undo log。從這個過程能夠發現mvcc保證一致性讀的過程當中會致使索引覆蓋失效。
先講講什麼是隔離級別Isolation,Isolation就是數據庫ACID中的I。隔離級別講的是不一樣的事務transaction在使用相同數據時之間的隔離程度。講到隔離級別其實有兩個層次,一個是SQL標準中規定的隔離級別,一個是數據庫對於隔離級別的實現。
第三版本sql修訂也就是sql-92中定義了4中隔離級別。
sql-92中是經過是否會產生一些不一致問題來定義了隔離級別,這些不一致問題有:dirty read(髒讀),none-repeatable read(不可重複讀)和 phantom read(幻讀)
Dirty Read髒讀定義最簡單是隻讀到了其餘trx還沒有commit的insert或者update。
不可重複讀和幻讀在筆者第一次看到時總以爲這兩個是一回事,不知道你們是否是有過和我同樣的疑惑。可是若是看了第一篇講鎖的狀況你們就知道對已經存在的行記錄records和還沒存在的行記錄(間隙)加鎖的狀況是徹底不同的。而不可重複度和幻讀也是根據records是否存在來區分的。
不可重複度是指已經一個trx中讀到的已存在的records被其餘trx修改或者刪除並體提交的記錄,也就是第一次讀到了數據再讀這個數據變化了或者沒了。幻讀是指一個trx中讀到了其餘trx insert並commit的記錄,也就是第一次讀沒有這個數據,再讀出現了。
在SQL-92中,隔離級別就根據是否能防止上述的三種問題來劃分(注意這個和innodb的實現不徹底一致)。Serializable定義成爲按照必定順序併發開始執行的事務與按一樣順序一個個順序執行的結果徹底相同,若是並行和串行徹底一致那麼就不會出現任何併發下的不一致問題。
這裏其實一個很重要的注意點就是這些定義更多的是針對none-locking reads也就是普通的select來說的,試想一下若是涉及到的行已經全都加上鎖了那麼就是最弱的隔離級別也不會出現髒讀了;爲何說是更多呢,不一樣的隔離級別對鎖的實現也不一樣,因此這3種不一致讀的現象在locking reads的狀況下也能夠參與討論。 sql-92定義的隔離級別能夠參考下圖:
innodb聲稱支持4種隔離級別但實現上的方案和效果與標準sql-92的定義不徹底一致,一個最大的差異就是innodb在RepetableRead的階段就防止了幻讀的出現。innodb實現的隔離級別能夠參考下圖,serializable並不經常使用其與repeatable相比就是把全部的select都變成了select for share從而把sql的並行化下降了。
在上一部分拋出的觀點是隔離級別所指的幾種不一致讀是在none-locking reads下的狀況,但隔離級別一樣影響locking reads的加鎖狀況,看過上篇的同窗應該有了解當咱們上一篇講insert intention lock、gap lock和next key lock這些對間隙才加鎖的。
因爲innodb下serializable隔離級別若是auto_commit = false會把none-locking的select都變成locking的select for share,若是auto_commit = true那麼每一個select都構成一個transaction而且立刻結束。因此這個隔離級別下沒有必要再討論none-locking reads。
在另外一個隔離級別read-uncommitted下,未提交的更改也能被別的trx看到,這個隔離級別下trx之間沒有相互consistent reading。
consistent none-locking reads的重點在於Read-committed(RC)和Repeatable-read(RR),這兩種隔離級別也是最經常使用的。RC和RR這兩種隔離級別下,none-locking reads使用上文所講的MVCC的機制來保證事務內部讀的一致性。只不過二者對於consistent的要求不一樣,RC認爲只要提交的讀均可以讀到因此RC的每次read其snaptshot都會更新,RR認爲在第一次讀讀到的snaptshot數據要堅持到trx結束。
這也就是爲何RC狀況下會產生幻讀和不可重複度而RR狀態下並不會的緣由:只要udpate和insert被commit,在RC狀況下都認爲能夠被讀到,RC的select會更新snapshot把這些update和insert的trx_id刷新進來。而因爲RR中的trx不會再更改第一次讀到的snapshot,因此update和insert就算成功了,因爲他們的trx_id並不在RR保存快照的範圍內,因此不會被讀到。
locking reads在不一樣的I級別下的加鎖狀態也不相同。RC和RR能夠在兩個方面對比學習。
RC級別下最大的特色是隻會加鎖record lock,並不會對間隙加鎖因此RC狀況下不能防止insert操做, 因此RC狀況下就算加了鎖能夠防止None-Repeatable Read,可是不能防止Phantom Read。
RR級別會給命中的record向前加next key lock,向後加gap lock到下一個record。從而RR級別能夠有效的防止Phantom Read。實際上根據innodb的實現一直防止insert到上一個record記錄和下一個record記錄,若是table中的表是十分稀疏的,被鎖定的區間會至關大。這也從另外一個角度說明了爲何使用transaction要儘量short and fast。
select * from tb_user_complaint where user_id = 222 for update;
+----+----------------------------+---------+------------------+------------+----------------------------+-----------+
| id | created_at | user_id | contents | is_archive | last_updated_at | user_name |
+----+----------------------------+---------+------------------+------------+----------------------------+-----------+
| 2 | 2020-02-12 15:12:19.214543 | 222 | complaint-test-1 | ^@ | 2020-02-12 15:12:19.214543 | macavity |
+----+----------------------------+---------+------------------+------------+----------------------------+-----------+
// RR級別下: (222,2)的X表示向前加了next-key lock,(500,1042)表示向後加了gap lock到這裏;
+--------+------------------------------------+-----------------------+-----------+----------+---------------+-------------------+----------------+-------------------+-------------+-----------------------+-----------+---------------+-------------+-----------+
| ENGINE | ENGINE_LOCK_ID | ENGINE_TRANSACTION_ID | THREAD_ID | EVENT_ID | OBJECT_SCHEMA | OBJECT_NAME | PARTITION_NAME | SUBPARTITION_NAME | INDEX_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+--------+------------------------------------+-----------------------+-----------+----------+---------------+-------------------+----------------+-------------------+-------------+-----------------------+-----------+---------------+-------------+-----------+
| INNODB | 4724256128:1079:140387850342152 | 6118 | 1068 | 134 | test | tb_user_complaint | <null> | <null> | <null> | 140387850342152 | TABLE | IX | GRANTED | <null> |
| INNODB | 4724256128:22:5:3:140387876554776 | 6118 | 1068 | 134 | test | tb_user_complaint | <null> | <null> | idx_user_id | 140387876554776 | RECORD | X | GRANTED | 222, 2 |
| INNODB | 4724256128:22:4:17:140387876555120 | 6118 | 1068 | 134 | test | tb_user_complaint | <null> | <null> | PRIMARY | 140387876555120 | RECORD | X,REC_NOT_GAP | GRANTED | 2 |
| INNODB | 4724256128:22:5:8:140387876555464 | 6118 | 1068 | 134 | test | tb_user_complaint | <null> | <null> | idx_user_id | 140387876555464 | RECORD | X,GAP | GRANTED | 500, 1042 |
+--------+------------------------------------+-----------------------+-----------+----------+---------------+-------------------+----------------+-------------------+-------------+-----------------------+-----------+---------------+-------------+-----------+
// RC級別下: 只有兩個index records lock
+--------+------------------------------------+-----------------------+-----------+----------+---------------+-------------------+----------------+-------------------+-------------+-----------------------+-----------+---------------+-------------+-----------+
| ENGINE | ENGINE_LOCK_ID | ENGINE_TRANSACTION_ID | THREAD_ID | EVENT_ID | OBJECT_SCHEMA | OBJECT_NAME | PARTITION_NAME | SUBPARTITION_NAME | INDEX_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+--------+------------------------------------+-----------------------+-----------+----------+---------------+-------------------+----------------+-------------------+-------------+-----------------------+-----------+---------------+-------------+-----------+
| INNODB | 4724256128:1079:140387850342152 | 6117 | 1068 | 129 | test | tb_user_complaint | <null> | <null> | <null> | 140387850342152 | TABLE | IX | GRANTED | <null> |
| INNODB | 4724256128:22:5:3:140387876554776 | 6117 | 1068 | 129 | test | tb_user_complaint | <null> | <null> | idx_user_id | 140387876554776 | RECORD | X,REC_NOT_GAP | GRANTED | 222, 2 |
| INNODB | 4724256128:22:4:17:140387876555120 | 6117 | 1068 | 129 | test | tb_user_complaint | <null> | <null> | PRIMARY | 140387876555120 | RECORD | X,REC_NOT_GAP | GRANTED | 2 |
+--------+------------------------------------+-----------------------+-----------+----------+---------------+-------------------+----------------+-------------------+-------------+-----------------------+-----------+---------------+-------------+-----------+
複製代碼
RR級別加間隙鎖的一個大坑是與1)執行計劃中使用的index是否unique,2)且對應的record是否能找到相關的:
// 使用pk搜索而且命中一條記錄,查看datalocks只有X,rec_not_gap鎖
select * from tb_user_complaint where id = 2 for update;
+----+----------------------------+---------+------------------+------------+----------------------------+-----------+
| id | created_at | user_id | contents | is_archive | last_updated_at | user_name |
+----+----------------------------+---------+------------------+------------+----------------------------+-----------+
| 2 | 2020-02-12 15:12:19.214543 | 222 | complaint-test-1 | ^@ | 2020-02-12 15:12:19.214543 | macavity |
+----+----------------------------+---------+------------------+------------+----------------------------+-----------+
Time: 0.013s
+--------+------------------------------------+-----------------------+-----------+----------+---------------+-------------------+----------------+-------------------+------------+-----------------------+-----------+---------------+-------------+-----------+
| ENGINE | ENGINE_LOCK_ID | ENGINE_TRANSACTION_ID | THREAD_ID | EVENT_ID | OBJECT_SCHEMA | OBJECT_NAME | PARTITION_NAME | SUBPARTITION_NAME | INDEX_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+--------+------------------------------------+-----------------------+-----------+----------+---------------+-------------------+----------------+-------------------+------------+-----------------------+-----------+---------------+-------------+-----------+
| INNODB | 4724256128:1079:140387850342152 | 6121 | 1068 | 145 | test | tb_user_complaint | <null> | <null> | <null> | 140387850342152 | TABLE | IX | GRANTED | <null> |
| INNODB | 4724256128:22:4:17:140387876554776 | 6121 | 1068 | 145 | test | tb_user_complaint | <null> | <null> | PRIMARY | 140387876554776 | RECORD | X,REC_NOT_GAP | GRANTED | 2 |
+--------+------------------------------------+-----------------------+-----------+----------+---------------+-------------------+----------------+-------------------+------------+-----------------------+-----------+---------------+-------------+-----------+
複製代碼
這個特色感受很容易被忽略。
第一階段: 次級索引只有索引自己和葉子節點primary key的信息,若是update和delete的where條件裏有其餘篩選條件的話次級索引並無法精確的篩選出命中行,這個時候innodb會選擇把全部粗糙命中的行加上record lock。
第二階段: 以後inndb會把命中的pk返回給mysql層,mysql層須要回表clustered index來獲取數據進行過濾。這裏不一樣的隔離級別對粗糙命中行的處理就不同了。RC級別會把經過回表過濾掉的record locks 釋放掉(注意第一階段就算是RC也會先申請鎖),RR級別會繼續保留全部的鎖直到trx結束。-0-
下邊咱們看個例子,咱們把where條件限制成 user_id和user_name,userId=555,userId被選爲執行計劃中的key。命中userId=555的有3行可是name符合條件的只有兩行。這個時候再RR和RC的加鎖的結果就徹底不一樣。
select * from tb_user_complaint where user_id = 555 and user_name = "macavity" for update;
+-----+----------------------------+---------+------------------+------------+----------------------------+-----------+
| id | created_at | user_id | contents | is_archive | last_updated_at | user_name |
+-----+----------------------------+---------+------------------+------------+----------------------------+-----------+
| 17 | 2020-02-15 16:08:00.184710 | 555 | complaint-test-1 | ^@ | 2020-02-15 16:08:00.184710 | macavity |
| 123 | 2020-02-12 15:12:11.922491 | 555 | complaint-test-1 | ^@ | 2020-02-14 13:40:04.517506 | macavity |
+-----+----------------------------+---------+------------------+------------+----------------------------+-----------+
複製代碼
RR狀況下會對全部userId命中的條件加鎖,同時注意鎖類型是X鎖也就是nextKey lock加鎖範圍很大。
RC狀況下,在回表進行where查詢以後會把以前錯加的鎖釋放掉,只會加真正命中的行鎖(注意沒有gap也沒有next-key lock)。
這裏仔細想一下,在RC條件下第一個階段若是須要加全部疑似的行鎖的話,那麼是否是兩個where語句即便在第二階段不衝突,第一階段的衝突也可能致使block?
在RC級別下面執行兩個語句,答案是trx-2是否會block麼?實際上是否衝突是和兩個執行計劃使用的key是強相關的,若是兩個sql都使用user_id做爲key那麼就會發生衝突,由於trx-1雖然在第二階段釋放了user_name=midofinos行,可是trx-2第一階段的粗糙加鎖回去請求macavity行的鎖。若是其中一個使用了user_name做爲key,那麼就不會block。
// 在RC條件下都指定使用userId做爲index,發現第一個trx沒有鎖住midofinos,可是第二個trx仍是會被block;
// 去掉index hint時innodb使用user_name做爲index,兩個trx再也不干擾彼此;
// trx-1
select * from tb_user_complaint use index (idx_user_id) where user_id = 555 and user_name = "macavity";
// trx-2
select * from tb_user_complaint use index (idx_user_id) where user_id = 555 and user_name = "midofinos";
複製代碼
至於uncommitted和serializable對於locking-reads的影響,能夠理解爲RU對於locking-reads和RC狀況相同,而serializable和RR狀況相同。這兩個級別是對於RC和RR更多的是在none-locking reads上一致性的放鬆/增強。
DB一直是筆者比較喜歡的課題,關於隔離級別和鎖就寫完了。但願你們喜歡:)