天天看點

Mysql資料一緻性與處理并發通路處理:隔離級别,鎖政策與MVCC

基礎概念

髒讀 一個事務中通路到了另外一個事務未送出的資料
不可重複讀 一個事務内根據同一個條件對行記錄進行多次查詢,傳回的結果不一緻
幻讀 同一個事務内多次查詢傳回的結果集不一樣(增加了或者減少)

隔離級别

隔離級别 描述
讀未送出(read uncommit) 一個事務還沒送出時,它做的變更就能被别的事務看到 任何操作都不會加鎖
讀送出(read commit) 一個事務送出之後,它做的變更才會被其他事務看到 在RC級别中,資料的讀取都是不加鎖的,但是資料的寫入、修改和删除是需要加鎖的
可重複讀(repeatable read) 一個事務執行過程中看到的資料,總是跟這個事務在啟動時看到的資料是一緻的 讀操作不需要加鎖,而寫操作需要加鎖。
串行化讀(serializable): 當出現讀寫鎖沖突的時候,後通路的事務必須等前一個事務執行完成,才能繼續執行。 “寫”會加“寫鎖”,“讀”會加“讀鎖”
隔離級别 髒讀 不可重複讀 幻讀
讀未送出(read uncommit) 可能 可能 可能
讀送出(read commit) 不可能 可能 可能
可重複讀(repeatable read) 不可能 不可能(MVCC實作) 不可能(用next-key lock 保證)
串行化讀(serializable) 不可能 不可能 不可能
  • mysql隔離級别有兩個作用域,一個是目前會話隔離級别,另一個是系統隔離級别。
  • 讀取資料時,資料庫會建立視圖,通路的時候以視圖的邏輯結果為準
隔離級别 視圖
讀未送出(read uncommit) 直接傳回記錄上的最新值,沒有視圖概念
讀送出(read commit) 視圖是在每個 SQL 語句開始執行的時候建立的
可重複讀(repeatable read) 視圖是在事務啟動時建立的,整個事務存在期間都用這個視圖
串行化讀(serializable) 直接用加鎖的方式來避免并行通路。

快照讀(snapshot read)和目前讀(current read)

MySQL中的目前讀和快照讀是指在讀取資料時的不同方式。目前讀(Current Read)是指讀取最新的資料,而快照讀(Snapshot Read)則是指讀取某個特定時刻的資料快照。這兩種讀操作在不同的隔離級别下有不同的行為。

目前讀(Current Read) 快照讀(Snapshot Read)
讀未送出(read uncommit) 讀取到其他事務未送出的資料(髒讀) 讀取到其他事務未送出的資料(髒讀)
讀送出(read commit) 目前讀操作會等待其他事務的鎖釋放,然後讀取已送出的最新版本的資料。 快照讀隻會讀取已經送出的資料。這意味着事務不會看到其他事務正在修改的資料。然而,同一事務内的多次讀操作可能會看到不同的資料版本,因為其他事務可能在此期間送出了修改。
可重複讀(repeatable read) 目前讀仍然會等待其他事務的鎖釋放,然後讀取已送出的最新版本的資料。在目前讀(current read)的情況下,MySQL通過next-key lock來避免幻讀 在可重複讀隔離級别下,事務在開始時會建立一個快照,所有快照讀操作都會基于這個快照讀取資料。在快照讀(snapshot read)的情況下,MySQL通過MVCC(多版本并發控制)來避免幻讀
串行化讀(serializable) 在串行化隔離級别下,所有事務都會串行執行,是以目前讀會讀取到已送出的最新版本的資料。 同目前讀

InnoDB 鎖機制

MySQL中的鎖有很多種,按照資源通路限制的不同程度,分為:

  1. 共享鎖(Shared Locks):也稱為讀鎖(Read Locks),允許多個事務同時讀取相同的資料,但在共享鎖生效期間,其他事務不能對該資料進行修改。共享鎖的目的是保證在一個事務讀取資料時,其他事務不能修改這些資料。
  2. 排他鎖(Exclusive Locks):也稱為寫鎖(Write Locks),當一個事務需要對資料進行修改時,會請求排他鎖。在排他鎖生效期間,其他事務既不能對該資料進行修改,也不能讀取這些資料。排他鎖的目的是保證在一個事務修改資料時,其他事務不能讀取或修改這些資料。

從鎖粒度的角度看,MySQL中有這麼幾種鎖:

  1. 表鎖(Table Locks):表鎖是MySQL中最基本的鎖政策,适用于MyISAM、MEMORY等存儲引擎。當一個事務需要對表進行操作時,會鎖定整個表,防止其他事務在鎖定期間對表進行操作。表鎖的粒度較大,導緻鎖沖突的機率較高,是以在高并發場景下,表鎖的性能較低。
  2. 行鎖(Row Locks):行鎖是MySQL中更進階的鎖政策,适用于InnoDB等存儲引擎。它允許對表中的單個行進行鎖定,減少鎖沖突的機率,提高并發性能。行鎖的粒度較小,但實作複雜,可能導緻死鎖(Deadlocks)等問題。

除了上述基本鎖類型外,MySQL還支援其他鎖政策,如間隙鎖(Gap Locks)、意向鎖(Intention Locks)等,以解決不同場景下的并發問題。

Record Locks (行鎖)

Record Locks,平時所說的行鎖,Record Locks是通過給索引上的索引項加鎖來實作的 這意味着:隻有通過索引條件檢索資料,InnoDB 才使用行級鎖,否則,InnoDB 将使用表鎖。

jsx複制代碼SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
           

上面這個語句會對t.c1 = 10記錄加排他鎖,其他事務對 t.c1 = 10 記錄的插入,更新和删除操作都會被阻塞。

在沒有定義索引的情況下,InnoDB 會建立一個隐藏的聚簇索引,并使用此索引進行記錄鎖定。

這是一段事務日志

jsx複制代碼
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 00000000274f; asc     'O;;
 2: len 7; hex b60000019d0110; asc        ;;

           

在事務日志中,行鎖一般用這種方式表示

jsx複制代碼lock_mode X locks rec but not gap
           

X 表示 Exclusive ,于此相對的還有S 表示Shared

Gap Lock (間隙鎖)

間隙鎖是對索引記錄之間的間隙的鎖定,或者是對第一個或最後一個索引記錄之前或之後的間隙的鎖定。例如,執行 SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; 可阻止其他事務向 t.c1 列插入值 15,無論該列中是否已存在此類值,因為範圍内所有現有值之間的間隙都被鎖定。

間隙可能包括一個索引值、多個索引值,甚至為空。

對于使用唯一索引搜尋唯一行的SQL,不需要間隙鎖定。例如,如果 id 列具有唯一索引,以下語句僅對具有 id 值 100 的行使用Record Locks,而不管其他會話是否在前面的間隙中插入行:

sql複制代碼SELECT * FROM child WHERE id = 100;
           

如果 id 沒有索引或具有非唯一索引,該語句确實會鎖定前面的間隙。

如果搜尋條件僅包含聯合唯一索引的某些列,還是會加上間隙鎖.

不同僚務可以在間隙上持有沖突的鎖。例如,事務 A 可以在間隙上持有共享間隙鎖(間隙 S-鎖),而事務 B 在同一間隙上持有排他間隙鎖(間隙 X-鎖)。跟間隙鎖存在沖突關系的,是“往這個間隙中插入一個記錄”這個操作。

一般情況下,間隙鎖隻作用在可重複讀隔離級别(RR)下. RC隔離級别下,在進行外鍵限制檢測和唯一鍵限制檢測的時候,會使用到Gap鎖。

Insert Intention Locks(插入意向鎖)

Insert Intention Locks 是一種插入意向鎖,它是在真正插入一行記錄之前由insert操作設定的.

隻要兩個事務插入的不是同一行,就不會互相阻塞。

插入意向鎖在事務日志中的表現:

jsx複制代碼lock_mode X locks gap before rec insert intention waiting
           

完整日志

jsx複制代碼RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child`
trx id 8731 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000066; asc    f;;
 1: len 6; hex 000000002215; asc     " ;;
 2: len 7; hex 9000000172011c; asc     r  ;;...

           

RC級别下使用Gap Lock的原因

RC級别下,進行唯一鍵限制檢測的時候需要使用到間隙鎖,是為了解決主從複制的一些bug,比如

當我們并發的用INSERT …ON DUPLICATE KEY UPDATE的時候,如果我們有多個唯一索引,那麼有可能會導緻binlog錯誤,也就是會導緻主從複制不一緻,具體可以參考 :bugs.mysql.com/bug.php?id=…

Gap Lock練習

現在一個表有三個字段 id a b 其中b有索引 現在裡面插入(5,5,5),(10,10,10),(15,15,15)

java複制代碼用戶端a:
begin;
select * from tb where b=10 for update;
           
java複制代碼用戶端b
begin;
select * from tb where b=9 for update;
           
  1. 問這個b會卡住 還是查出來空:查出來為空
  2. insert (9,9,9) 會發生什麼: 會卡死

Next-key lock

Next-key 鎖是一種結合了Record Locks (行鎖) 和Gap Locks (間隙鎖)的鎖定機制,它鎖定一個索引記錄以及記錄之前的間隙.

假設我們有一個按 c1 列排序的索引,索引中的記錄值為

sql複制代碼5, 10, 20, 30, 40
           

對于查詢 SELECT * FROM t WHERE c1 BETWEEN 10 AND 30 FOR UPDATE;InnoDB 會鎖定值為 10、20 和 30 的記錄,并鎖定它們之間的間隙。鎖定範圍如下:

sql複制代碼(5, 10], [10, 20], [20, 30]
           

假如索引記錄為

sql複制代碼10, 20, 30, 40
           

鎖定範圍如下:

sql複制代碼(-∞, 10], [10, 20], [20, 30]
           

next-key lock的事務資料在SHOW ENGINE INNODB STATUS和INNODB螢幕輸出中顯示如下:

java複制代碼RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10080 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 6; hex 00000000274f; asc     'O;;
 2: len 7; hex b60000019d0110; asc        ;;

           

這裡的lock_mode X可以認為就是Next-Key lock

Next-key lock引發的死鎖case

SQL : insert on duplicate key update

死鎖現場

java複制代碼LATEST DETECTED DEADLOCK
------------------------
2021-08-13 16:00:22 0x7fe08f17d700
*** (1) TRANSACTION:
TRANSACTION 10992144786, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 48550292, OS thread handle 140557710284544, query id 51167478136 10.78.184.193 dev_1597323979 update
/*id:8307ed81*/insert into store_visit_info
         ( mall_id,
                has_wechat_group,
                wechat_group_number,
                store_id )
         values ( 614802394,
                1,
                1,
                1952182965122 )
         ON DUPLICATE KEY UPDATE mall_id = 614802394,

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 771 page no 6523 n bits 776 index uk_mall_id of table `sanmateo`.`store_visit_info` trx id 10992144786 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 596 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 8; hex 0000000024a523e3; asc     $ # ;;
 1: len 8; hex 00000000001548d6; asc       H ;;

*** (2) TRANSACTION:
TRANSACTION 10992144785, ACTIVE 0 sec updating or deleting, thread declared inside InnoDB 4998
mysql tables in use 1, locked 1
6 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1
MySQL thread id 48549906, OS thread handle 140602450106112, query id 51167478135 10.78.184.193 dev_1597323979 update
/*id:8307ed81*/insert into store_visit_info
         ( mall_id,
                has_wechat_group,
                wechat_group_number,
                store_id )
         values ( 614802394,
                1,
                1,
                1952182965122 )
         ON DUPLICATE KEY UPDATE mall_id = 614802394,
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 771 page no 6523 n bits 776 index uk_mall_id of table `sanmateo`.`store_visit_info` trx id 10992144785 lock_mode X locks gap before rec
Record lock, heap no 596 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 8; hex 0000000024a523e3; asc     $ # ;;
 1: len 8; hex 00000000001548d6; asc       H ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 771 page no 6523 n bits 776 index uk_mall_id of table `sanmateo`.`store_visit_info` trx id 10992144785 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 596 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 8; hex 0000000024a523e3; asc     $ # ;;
 1: len 8; hex 00000000001548d6; asc       H ;;

*** WE ROLL BACK TRANSACTION (1)
------------
TRANSACTIONS
------------
           

死鎖原因分析

  • insert on duplicate key update ,當檢測到唯一鍵沖突的時候,會在出現沖突的唯一索引處加上next-key lock。
  • 根據上面的日志,我們可以知道,兩個事務成功擷取到同一段間隙的next-key lock,嘗試寫入資料,在寫入資料前需要插入意向鎖,而意向鎖的插入需要等待排他鎖的釋放,事務10992144786在等待事務10992144785釋放排他鎖,而事務10992144785在等待事務10992144786釋放排他鎖,形成死鎖。

解決方案 在有競争條件下,我們很難避免死鎖的形成,我們的資料庫應對死鎖的政策是主動復原其中一個事務,是以我們重點關注事物復原帶來的資料丢失問題。

Metadata Locking 與 DDL

中繼資料鎖定(Metadata Locking)是一種同步機制,用于確定多個并發會話在通路和修改資料庫對象(如表、視圖以及存儲過程等)時能夠保持一緻性和完整性。中繼資料鎖定通過為這些對象添加特定類型的鎖來實作,例如共享鎖(Shared Locks)和排他鎖(Exclusive Locks)。

當對一個表做增删改查操作的時候,加 MDL 讀鎖;當要對表做結構變更操作的時候,加 MDL 寫鎖。

以下是DDL操作過程中中繼資料鎖定的一些典型行為:

  • 修改表結構(ALTER TABLE):在執行ALTER TABLE操作時,MySQL會為要修改的表設定一個排他鎖。在表結構修改期間,其他會話無法通路或修改該表。這確定了表結構更改過程中資料的完整性。一旦表結構修改完成,排他鎖會被釋放,其他會話可以繼續通路表。
  • 建立/修改/删除索引(CREATE INDEX、ALTER INDEX、DROP INDEX):在執行與索引相關的DDL操作時,MySQL會為涉及的表設定一個排他鎖。在索引更改期間,其他會話無法通路或修改該表。這確定了在建立、修改或删除索引時資料的一緻性。操作完成後,排他鎖會被釋放,其他會話可以繼續通路表。

Online DDL

MySQL 5.6中引入Online DDL功能, 它允許在表結構修改過程中,表仍然可以接受讀取和寫入操作。這種方法可以減少由于DDL操作導緻的表鎖定時間,提高資料庫的可用性和并發性能。

傳統的DDL操作,如ALTER TABLE,通常需要在整個操作過程中對表加鎖,進而阻止其他會話執行讀取或寫入操作。線上DDL通過允許表在結構更改過程中繼續接受讀取和寫入操作,可以顯著減少鎖定時間。然而,并非所有的ALTER TABLE操作都可以作為線上DDL執行。針對InnoDB表的支援取決于操作類型、索引類型等因素。

Online DDL的行文和存儲引擎,DDL類型,DML操作類型有關

線上DDL的一些關鍵特性:

  1. 隐式鎖定:在執行線上DDL操作時,MySQL會隐式地為涉及的表添加中繼資料鎖。這些中繼資料鎖會在DDL操作持續期間保持,以確定其他會話不會執行與目前DDL操作沖突的操作,如另一個ALTER TABLE操作。中繼資料鎖通常比傳統DDL操作中的表鎖定更加輕量級,對資料庫性能的影響較小。
  2. 鎖定粒度:線上DDL試圖以更細的鎖定粒度來降低鎖定沖突。例如,對于某些操作,它可能隻鎖定表的某個部分,而不是整個表。這允許其他會話在DDL操作執行期間通路和修改表的未鎖定部分。然而,并非所有線上DDL操作都支援細粒度鎖定,某些情況下仍可能需要對整個表加鎖。
  3. 并發讀取和寫入:線上DDL允許在表結構修改過程中執行SELECT和INSERT、UPDATE、DELETE等DML操作。這是通過在操作過程中使用共享鎖和排他鎖來實作的。例如,在添加或删除索引時,MySQL可能會使用共享鎖來允許其他會話繼續讀取資料,但在此過程中阻止寫操作。這有助于避免長時間的表鎖定。
  4. 操作階段:線上DDL操作可以分為多個階段,每個階段可能涉及不同類型的鎖定。例如,某些操作可能在開始階段需要短暫的排他鎖,然後在操作的其餘部分使用共享鎖。這可以進一步減少鎖定時間,提高資料庫的可用性。
  5. 操作中斷:線上DDL支援在操作過程中暫停和恢複,以便在需要時中斷長時間運作的DDL操作。

Online DDL操作階段

  1. 準備階段:在此階段,MySQL為執行DDL操作做好準備。這可能包括對表結構和限制進行一些初始檢查,以確定操作可以安全地進行。此階段通常不需要對表進行鎖定。
  2. 建立新結構階段:在此階段,MySQL建立一個新的表結構,以便在操作過程中維護資料的一緻性。新結構通常是舊結構的副本,但包含更改後的列、索引等。在某些情況下,這可能需要在舊表上設定共享鎖,以允許其他會話繼續讀取資料,但阻止寫操作。此階段的鎖定時間可能較短。
  3. 資料複制階段:在此階段,MySQL将資料從舊表複制到新結構。根據操作的類型和表的大小,這可能需要一定的時間。在資料複制期間,MySQL可能會為舊表和新結構設定共享鎖和排他鎖,以確定資料一緻性。然而,這些鎖定通常具有較細的粒度,進而允許其他會話在操作過程中繼續通路和修改未鎖定的部分。
  4. 切換結構階段:在此階段,MySQL将新結構替換為舊表。這可能需要在表上設定短暫的排他鎖,以確定其他會話在切換過程中不會通路或修改資料。一旦新結構成功替換舊表,鎖定會被釋放,其他會話可以繼續通路和修改新表。
  5. 清理階段:在此階段,MySQL完成操作的最終步驟,例如删除舊表、更新中繼資料和統計資訊等。此階段通常不需要對表進行鎖定。

死鎖的定義與解決政策

死鎖:兩個或兩個以上的程序或事務互相等待

政策:

  1. 一種政策是,直接進入等待,直到逾時。這個逾時時間可以通過參數 innodb_lock_wait_timeout 來設定。
  2. 另一種政策是,發起死鎖檢測,發現死鎖後,主動復原死鎖鍊條中的某一個事務,讓其他事務得以繼續執行。将參數 innodb_deadlock_detect 設定為 on,表示開啟這個邏輯。

MVCC 多版本并發控制

多版本并發控制(MVCC,Multi-Version Concurrency Control)是一種用于資料庫管理系統的并發控制技術。它允許多個事務同時通路和修改資料,而無需等待鎖定。MVCC通過為每個事務生成資料的"快照"來實作這一目标,進而使事務能夠獨立于其他事務工作,而不會互相幹擾。

MVCC的核心思想是在事務開始時,為每個讀取的資料行建立一個版本。事務會看到這個版本,而不是實際的資料行。這樣,事務可以在不影響其他事務的情況下進行讀取和修改操作。

在InnoDB中,會在每行資料後添加兩個額外的隐藏的值來實作MVCC,這兩個值一個記錄這行資料何時被建立,另外一個記錄這行資料何時過期(或者被删除)。 在實際操作中,存儲的并不是時間,而是事務的版本号,每開啟一個新事務,事務的版本号就會遞增。 在可重讀Repeatable reads事務隔離級别下:

  • SELECT時,讀取建立版本号<=目前事務版本号,删除版本号為空或>目前事務版本号。
  • INSERT時,儲存目前事務版本号為行的建立版本号
  • DELETE時,儲存目前事務版本号為行的删除版本号
  • UPDATE時,插入一條新紀錄,儲存目前事務版本号為行建立版本号,同時儲存目前事務版本号到原來删除的行

MVCC在InnoDB中的實作方式

Read View 主要來幫我們解決可見性的問題的, 即他會來告訴我們本次事務應該看到哪個快照,不應該看到哪個快照。

在 Read View 中有幾個重要的屬性:

  • trx_ids,系統目前未送出的事務 ID 的清單。
  • low_limit_id,未送出的事務中最大的事務 ID。
  • up_limit_id,未送出的事務中最小的事務 ID。
  • creator_trx_id,建立這個 Read View 的事務 ID。

Innodb存儲引擎中,每行資料都包含了一些隐藏字段:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR和DELETE_BIT。

Mysql資料一緻性與處理并發通路處理:隔離級别,鎖政策與MVCC

DB_TRX_ID:用來辨別最近一次對本行記錄做修改的事務的辨別符,即最後一次修改本行記錄的事務id。delete操作在内部來看是一次update操作,更新行中的删除辨別位DELELE_BIT。DB_ROLL_PTR:指向目前資料的undo log記錄,復原資料通過這個指針來尋找記錄被更新之前的内容資訊。

一行資料會對應多行這樣的記錄,例如,如果有多個事物對同一行資料進行更新,會形成這樣的記

Mysql資料一緻性與處理并發通路處理:隔離級别,鎖政策與MVCC

事物啟動的時候,mysql會為這個事物建立一個數組A,數組的元素為該事務啟動瞬間,系統中啟動了但還沒送出的所有事務 ,數組中事務id的最小值記為low_limit_id,目前系統裡面已經建立過的事務 ID 的最大值加 1 記為up_limit_id

讀取某一行資料,如果該資料最新的事務ID小于low_limit_id 那麼該版本是可見的

如果事務ID大于up_limit_id 該版本不可見 根據復原指針找到上一個版本記錄

如果事務ID落在low_limit_id和up_limit_id 之間 如果A中包含該事務ID,該版本不可見,未包含該事務ID 該版本可見

原文連結:https://juejin.cn/post/7251501945272844348

繼續閱讀