MySQL鎖詳解

根據加鎖的範圍,MySQL裏面的鎖大致可以分成全局鎖、表級鎖和行鎖三類

一、全局鎖

全局鎖就是對整個數據庫實例加鎖。MySQL提供了一個加全局讀鎖的方法,命令是Flush tables with read lock。當需要讓整個庫處於只讀狀態的時候,可以使用這個命令,之後其他線程的以下語句會被阻塞:數據更新語句(數據的增刪改)、數據定義語句(包括建表、修改表結構等)和更新類事務的提交語句

全局鎖的典型使用場景是,做全庫邏輯備份。也就是把整庫每個表都select出來存成文本

但是讓整個庫都只讀,可能出現以下問題:

  • 如果在主庫上備份,那麼在備份期間都不能執行更新,業務基本上就得停擺
  • 如果在從庫上備份,那麼在備份期間從庫不能執行主庫同步過來的binlog,會導致主從延遲

在可重複讀隔離級別下開啓一個事務能夠拿到一致性視圖

官方自帶的邏輯備份工具是mysqldump。當mysqldump使用參數–single-transaction的時候,導數據之前就會啓動一個事務,來確保拿到一致性視圖。而由於MVCC的支持,這個過程中數據是可以正常更新的。single-transaction只適用於所有的表使用事務引擎的庫

1.既然要全庫只讀,爲什麼不使用set global readonly=true的方式?

  • 在有些系統中,readonly的值會被用來做其他邏輯,比如用來判斷一個庫是主庫還是備庫。因此修改global變量的方式影響面更大
  • 在異常處理機制上有差異。如果執行Flush tables with read lock命令之後由於客戶端發生異常斷開,那麼MySQL會自動釋放這個全局鎖,整個庫回到可以正常更新的狀態。而將整個庫設置爲readonly之後,如果客戶端發生異常,則數據庫會一直保持readonly狀態,這樣會導致整個庫長時間處於不可寫狀態,風險較高

二、表級鎖

MySQL裏面表級別的鎖有兩種:一種是表鎖,一種是元數據鎖(meta data lock,MDL)

表鎖的語法是lock tables … read/write。可以用unlock tables主動釋放鎖,也可以在客戶端斷開的時候自動釋放。lock tables語法除了會限制別的線程的讀寫外,也限定了本線程接下來的操作對象

如果在某個線程A中執行lock tables t1 read,t2 wirte;這個語句,則其他線程寫t1、讀寫t2的語句都會被阻塞。同時,線程A在執行unlock tables之前,也只能執行讀t1、讀寫t2的操作。連寫t1都不允許

另一類表級的鎖是MDL。MDL不需要顯式使用,在訪問一個表的時候會被自動加上。MDL的作用是,保證讀寫的正確性。如果一個查詢正在遍歷一個表中的數據,而執行期間另一個線程對這個表結構做了變更,刪了一列,那麼查詢線程拿到的結果跟表結構對不上,肯定不行

在MySQL5.5版本引入了MDL,當對一個表做增刪改查操作的時候,加MDL讀鎖;當要對錶做結構變更操作的時候,加MDL寫鎖

  • 讀鎖之間不互斥,因此可以有多個線程同時對一張表增刪改查
  • 讀寫鎖之間、寫鎖之間是互斥的,用來保證變更表結構操作的安全性。因此,如果有兩個線程要同時給一個表加字段,其中一個要等另一個執行完才能開始執行

給一個表加字段,或者修改字段,或者加索引,需要掃描全表的數據。在對大表操作的時候,需要特別小心,以免對線上服務造成影響
在這裏插入圖片描述
session A先啓動,這時候會對錶t加一個MDL讀鎖。由於session B需要的也是MDL讀鎖,因此可以正常執行。之後sesession C會被blocked,是因爲session A的MDL讀鎖還沒有釋放,而session C需要MDL寫鎖,因此只能被阻塞。如果只有session C自己被阻塞還沒什麼關係,但是之後所有要在表t上新申請MDL讀鎖的請求也會被session C阻塞。所有對錶的增刪改查操作都需要先申請MDL讀鎖,就都被鎖住,等於這個表現在完全不可讀寫了

事務中的MDL鎖,在語句執行開始時申請,但是語句結束後並不會馬上釋放,而會等到整個事務提交後再釋放

1.如果安全地給小表加字段?

首先要解決長事務,事務不提交,就會一直佔着DML鎖。在MySQL的information_schema庫的innodb_trx表中,可以查到當前執行的事務。如果要做DDL變更的表剛好有長事務在執行,要考慮先暫停DDL,或者kill掉這個長事務

2.如果要變更的表是一個熱點表,雖然數據量不大,但是上面的請求很頻繁,而又不得不加個字段,該怎麼做?

在alter table語句裏面設定等待時間,如果在這個指定的等待時間裏面能夠拿到MDL寫鎖最好,拿不到也不要阻塞後面的業務語句,先放棄。之後再通過重試命令重複這個過程

三、行鎖

MySQL的行鎖是在引擎層由各個引擎自己實現的。但不是所有的引擎都支持行鎖,比如MyISAM引擎就不支持行鎖

行鎖就是針對數據表中行記錄的鎖。比如事務A更新了一行,而這時候事務B也要更新同一行,則必須等事務A的操作完成後才能進行更新

1、兩階段鎖協議

在這裏插入圖片描述
事務A持有的兩個記錄的行鎖都是在commit的時候才釋放的,事務B的update語句會被阻塞,直到事務A執行commit之後,事務B才能繼續執行

在InnoDB事務中,行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議

如果事務中需要鎖多個行,要把最可能造成鎖衝突、最可能影響併發度的鎖儘量往後放

假設要實現一個電影票在線交易業務,顧客A要在影院B購買電影票。業務需要涉及到以下操作:

1.從顧客A賬戶餘額中扣除電影票價

2.給影院B的賬戶餘額增加這張電影票價

3.記錄一條交易日誌

爲了保證交易的原子性,要把這三個操作放在一個事務中。如何安排這三個語句在事務中的順序呢?

如果同時有另外一個顧客C要在影院B買票,那麼這兩個事務衝突的部分就是語句2了。因爲它們要更新同一個影院賬戶的餘額,需要修改同一行數據。根據兩階段鎖協議,所有的操作需要的行鎖都是在事務提交的時候才釋放的。所以,如果把語句2安排在最後,比如按照3、1、2這樣的順序,那麼影院賬戶餘額這一行的鎖時間就最少。這就最大程度地減少了事務之間的鎖等待,提升了併發度

2、死鎖和死鎖檢測

在併發系統中不同線程出現循環資源依賴,涉及的線程都在等待別的線程釋放資源時,就會導致這幾個線程都進入無限等待的狀態,稱爲死鎖
在這裏插入圖片描述
事務A在等待事務B釋放id=2的行鎖,而事務B在等待事務A釋放id=1的行鎖。事務A和事務B在互相等待對方的資源釋放,就是進入了死鎖狀態。當出現死鎖以後,有兩種策略:

  • 一種策略是,直接進入等待,直到超時。這個超時時間可以通過參數innodb_lock_wait_timeout來設置
  • 另一種策略是,發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事務,讓其他事務得以繼續執行。將參數innodb_deadlock_detect設置爲on,表示開啓這個邏輯

在InnoDB中,innodb_lock_wait_timeout的默認值是50s,意味着如果採用第一個策略,當出現死鎖以後,第一個被鎖住的線程要過50s纔會超時退出,然後其他線程纔有可能繼續執行。對於在線服務來說,這個等待時間往往是無法接受的

正常情況下還是要採用主動死鎖檢查策略,而且innodb_deadlock_detect的默認值本身就是on。主動死鎖監測在發生死鎖的時候,是能夠快速發現並進行處理的,但是它有額外負擔的。每當一個事務被鎖的時候,就要看看它所依賴的線程有沒有被別人鎖住,如此循環,最後判斷是否出現了循環等待,也就是死鎖

如果所有事務都要更新同一行的場景,每個新來的被堵住的線程都要判斷會不會由於自己的加入導致死鎖,這是一個時間複雜度是O(n)的操作

怎麼解決由這種熱點行更新導致的性能問題?

1.如果確保這個業務一定不會出現死鎖,可以臨時把死鎖檢測關掉

2.控制併發度

3.將一行改成邏輯上的多行來減少鎖衝突。以影院賬戶爲例,可以考慮放在多條記錄上,比如10個記錄,影院的賬戶總額等於這10個記錄的值的總和。這樣每次要給影院賬戶加金額的時候,隨機選其中一條記錄來加。這樣每次衝突概率變成員原來的1/10,可以減少鎖等待個數,也就減少了死鎖檢測的CPU消耗

四、爲什麼我只查一行的語句,也執行這麼慢?

構造一個表,這個表有兩個字段id和c,並且在裏面插入了10萬行記錄

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

CREATE DEFINER=`root`@`%` PROCEDURE `idata`()
BEGIN
	declare i int;
  set i=1;
  while(i<=100000) do
    insert into t values(i,i);
    set i=i+1;
  end while;
END

1、第一類:查詢長時間不返回

select * from t3 where id=1;

查詢結果長時間不返回,使用show processlist命令,查看當前語句處於什麼狀態

1)、等MDL鎖

如下圖所示,使用show processlist;命令查看Waiting for table metadata lock的示意圖
在這裏插入圖片描述
這個狀態表示現在有一個線程正在表t上請求或者持有MDL寫鎖,把select語句堵住了

場景復現:
在這裏插入圖片描述
sessionA通過lock table命令持有表t的MDL寫鎖,而sessionB的查詢需要獲取MDL讀鎖。所以,sessionB進入等待狀態

這類問題的處理方式,就是找到誰持有MDL寫鎖,然後把它kill掉。但是由於show processlist的結果裏,sessionA的Command列是Sleep,導致查找起來很不方便,可以通過查詢sys.schema_table_lock_waits這張表直接找出造成阻塞的process id,把這個連接kill命令斷開即可(MySQL啓動時需要設置performance_schema=on,相比於設置爲off會有10%左右的性能損失)

select blocking_pid from sys.schema_table_lock_waits;

2)、等flush

在表t上執行如下的SQL語句:

select * from information_schema.processlist where id=1;

查出來某個線程狀態爲Waiting for table flush
在這裏插入圖片描述
這個狀態表示的是,現在有一個線程政要對錶t做flush操作。MySQL裏面對錶做flush操作的用法,一般有以下兩個:

flush tables t with read lock;

flush tables with read lock;

這兩個flush語句,如果指定表t的話,代表的是隻關閉表t;如果沒有指定具體的表名,則表示關閉MySQL裏所有打開的表

但是正常情況下這兩個語句執行起來都很快,除非它們被別的線程堵住了

所以,出現Waiting for table flush狀態的可能情況是:有一個flush tables命令被別的語句堵住了,然後它有堵住了select語句

場景復現:
在這裏插入圖片描述
sessionA中,每行調用一次sleep(1),這樣這個語句默認要執行10萬秒,在這期間表t一直是被sessionA打開着。然後,sessionB的flush tables t再去關閉表t,就需要等sessionA的查詢結束。這樣sessionC要再次查詢的話,就會被flush命令堵住了
在這裏插入圖片描述
3)、等行鎖

select * from t where id=1 lock in share mode; 

由於訪問id=1這個記錄時要加讀鎖,如果這時候已經有一個事務在這行記錄上持有一個寫鎖,select語句就會被堵住

場景復現:
在這裏插入圖片描述在這裏插入圖片描述
sessionA啓動了事務,佔有寫鎖,還不提交,是導致sessionB被堵住的原因

2、第二類:查詢慢

在這裏插入圖片描述
sessionA先用start transaction with consistent snapshot命令開啓一個事務,建立事務的一致性讀(又稱爲快照讀。使用的是MVCC機制讀取undo log中的已經提交的數據。所以它的讀取是非阻塞的),之後sessionB執行update語句

sessionB執行完100萬次update語句後,生成100萬個回滾日誌
在這裏插入圖片描述
帶lock in share mode的語句是當前讀,因此會直接讀到1000001這個結果,速度很快;而select * from t where id=1這個語句是一致性讀,因此需要從1000001開始,依次執行undo log,執行了100萬次以後,纔將1這個結果返回

五、間隙鎖

建表和初始化語句如下:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

這個表除了主鍵id外,還有一個索引c

爲了解決幻讀問題,InnoDB引入了間隙鎖,鎖的就是兩個值之間的空隙
在這裏插入圖片描述
當執行select * from t where d=5 for update的時候,就不止是給數據庫中已有的6個記錄加上了行鎖,還同時加了7個間隙鎖。這樣就確保了無法再插入新的記錄

行鎖分成讀鎖和寫鎖
在這裏插入圖片描述
跟間隙鎖存在衝突關係的是往這個間隙中插入一個記錄這個操作。間隙鎖之間不存在衝突關係
在這裏插入圖片描述
這裏sessionB並不會被堵住。因爲表t裏面並沒有c=7會這個記錄,因此sessionA加的是間隙鎖(5,10)。而sessionB也是在這個間隙加的間隙鎖。它們用共同的目標,保護這個間隙,不允許插入值。但它們之間是不衝突的

間隙鎖和行鎖合稱next-key lock,每個next-key lock是前開後閉區間。表t初始化以後,如果用select * from t for update要把整個表所有記錄鎖起來,就形成了7個next-key lock,分別是(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。因爲+∞是開區間,在實現上,InnoDB給每個索引加了一個不存在的最大值supremum,這樣才符合都是前開後閉區間

間隙鎖和next-key lock的引入,解決了幻讀的問題,但同時也帶來了一些困擾

間隙鎖導致的死鎖:
在這裏插入圖片描述
1.sessionA執行select … for update語句,由於id=9這一行並不存在,因此會加上間隙鎖(5,10)

2.sessionB執行select … for update語句,同樣會加上間隙鎖(5,10),間隙鎖之間不會衝突

3.sessionB試圖插入一行(9,9,9),被sessionA的間隙鎖擋住了,只好進入等待

4.sessionA試圖插入一行(9,9,9),被sessionB的間隙鎖擋住了

兩個session進入互相等待狀態,形成了死鎖

間隙鎖的引入可能會導致同樣的語句鎖住更大的範圍,這其實是影響併發度的

在讀提交隔離級別下,不存在間隙鎖

六、next-key lock

表t的建表語句和初始化語句如下:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

1、next-key lock加鎖規則

  • 原則1:加鎖的基本單位是next-key lock,next-key lock是前開後閉區間
  • 原則2:查找過程中訪問到的對象纔會加鎖
  • 優化1:索引上的等值查詢,給唯一索引加鎖的時候,next-key lock退化爲行鎖
  • 優化2:索引上的等值查詢,向右遍歷時且最後一個值不滿足等值條件的時候,next-key lock退化爲間隙鎖
  • 一個bug:唯一索引上的範圍查詢會訪問到不滿足條件的第一個值爲止

這個規則只限於MySQL5.x系列<=5.7.24,8.0系列<=8.0.13

2、案例一:等值查詢間隙鎖

在這裏插入圖片描述
1.由於表t中沒有id=7的記錄,根據原則1,加鎖單位是next-key lock,sessionA加鎖範圍就是(5,10]

2.根據優化2,這是一個等值查詢(id=7),而id=10不滿足查詢條件,next-key lock退化成間隙鎖,因此最終加鎖的範圍是(5,10)

所以,sessionB要往這個間隙裏面插入id=8的記錄會被鎖住,但是sessionC修改id=10這行是可以的

3、案例二:非唯一索引等值鎖

在這裏插入圖片描述
1.根據原則1,加鎖單位是next-key lock,因此會給(0,5]加上next-key lock

2.c是普通索引,因此訪問c=5這一條記錄是不能馬上停下來的,需要向右遍歷,查到c=10才放棄。根據原則2,訪問到的都要加鎖,因此要給(5,10]加next-key lock

3.根據優化2,等值判斷,向右遍歷,最後一個值不滿足c=5這個等值條件,因此退化成間隙鎖(5,10)

4.根據原則2,只有訪問到的對象纔會加鎖,這個查詢使用覆蓋索引,並不需要訪問主鍵索引,所以主鍵索引上沒有任何鎖,這就是爲什麼sessionB的update語句可以執行完成

鎖是加在索引上的,在這個例子中,lock in share mode只鎖覆蓋索引,但是如果是for update,系統會認爲你接下來要更新數據,因此會順便給主鍵索引上滿足條件的行加上行鎖,這樣的話sessionB的update語句會被阻塞住。如果你要用 lock in share mode 來給行加讀鎖避免數據被更新的話,就必須得繞過覆蓋索引的優化,在查詢字段中加入索引中不存在的字段

4、案例三:主鍵索引範圍鎖

在這裏插入圖片描述
1.開始執行的時候,要找到第一個id=10的行,因此本該是next-key lock(5,10]。根據優化1,主鍵id上的等值條件,退化成行鎖,只加了id=10這一行的行鎖

2.範圍查詢就往後繼續找,找到id=15這一行停下來,因此需要加next-key lock(10,15]

所以,sessionA這時候鎖的範圍就是主鍵索引上,行鎖id=10和next-key lock(10,15]

5、案例四:非唯一索引範圍鎖

在這裏插入圖片描述
這次sessionA用字段c來判斷,加鎖規則跟案例三唯一的不同是:在第一次用c=10定位記錄的時候,索引c上加上(5,10]這個next-key lock後,由於索引c是非唯一索引,沒有優化規則,因此最終sessionA加的鎖是索引c上的(5,10]和(10,15]這兩個next-key lock

6、案例五:唯一索引範圍鎖bug

在這裏插入圖片描述
sessionA是一個範圍查詢,按照原則1的話,應該是索引id上只加(10,15]這個next-key lock,並且因爲id是唯一鍵,所以循環判斷到id=15這一行就應該停止了

但是實現上,InnoDB會掃描到第一個不滿足條件的行爲止,也就是id=20。而且由於這是個範圍掃描,因此索引id上的(15,20]這個next-key lock也會被鎖上

所以,sessionB要更新id=20這一行是會被鎖住的。同樣地,sessionC要插入id=16的一行,也會被鎖住

7、案例六:非唯一索引上存在等值的例子

insert into t values(30,10,30);

新插入的這一行c=10,現在表裏有兩個c=10的行。雖然有兩個c=10,但是它們的主鍵值id是不同的,因此這兩個c=10的記錄之間也是有間隙的
在這裏插入圖片描述在這裏插入圖片描述
sessionA在遍歷的時候,先訪問第一個c=10的記錄。根據原則1,這裏加的是(c=5,id=5)到(c=10,id=10)這個next-key lock。然後sessionA向右查找,直到碰到(c=15,id=15)這一行,循環才結束。根據優化2,這是一個等值查詢,向右查找到了不滿足條件的行,所以會退化成(c=10,id=10)到(c=15,id=15)的間隙鎖

也就是說,這個delete語句在索引c上的加鎖範圍,就是下圖中藍色區域覆蓋的部分,這個藍色區域左右兩邊都是虛線,表示開區間
在這裏插入圖片描述

8、案例七:limit語句加鎖

在這裏插入圖片描述
加了limit 2的限制,因此在遍歷到(c=10,id=30)這一行之後,滿足條件的語句已經有兩條,循環就結束了。因此,索引c上的加鎖範圍就變成了從(c=5,id=5)到(c=10,id=30)這個前開後閉區間,如下圖所示:
在這裏插入圖片描述
再刪除數據的時候儘量加limit,這樣不僅可以控制刪除數據的條數,讓操作更安全,還可以減小加鎖的範圍

9、案例八:一個死鎖的例子

在這裏插入圖片描述
1.sessionA啓動事務後執行查詢語句加lock in share mode,在索引c上加了next-key lock(5,10]和間隙鎖(10,15)

2.sessionB的update語句也要在索引c上加next-key lock(5,10],進入鎖等待

3.然後sessionA要再插入(8,8,8)這一行,被sessionB的間隙鎖鎖住。由於出現了死鎖,InnoDB讓sessionB回滾

sessionB的加next-key lock(5,10]操作,實際上分成了兩步,先是加(5,10)間隙鎖,加鎖成功;然後加c=10的行鎖,這時候才被鎖住的

七、用動態的觀點看加鎖

表t的建表語句和初始化語句如下:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

1、不等號條件裏的等值查詢

begin;
select * from t where id>9 and id<12 order by id desc for update;

利用上面的加鎖規則,這個語句的加鎖範圍是主鍵索引上的(0,5]、(5,10]和(10,15)。加鎖單位是next-key lock,這裏用到了優化2,即索引上的等值查詢,向右遍歷的時候id=15不滿足條件,所以next-key lock退化爲了間隙鎖(10,15)
在這裏插入圖片描述
1.首先這個查詢語句的語義是order by id desc,要拿到滿足條件的所有行,優化器必須先找到第一個id<12的值

2.這個過程是通過索引樹的搜索過程得到的,在引擎內部,其實是要找到id=12的這個值,只是最終沒找到,但找到了(10,15)這個間隙

3.然後根據order by id desc,再向左遍歷,在遍歷過程中,就不是等值查詢了,會掃描到id=5這一行,所以會加一個next-key lock (0,5]

在執行過程中,通過樹搜索的方式定位記錄的時候,用的是等值查詢的方法

2、等值查詢的過程

begin;
select id from t where c in(5,20,10) lock in share mode;

在這裏插入圖片描述
這條in語句使用了索引c並且rows=3,說明這三個值都是通過B+樹搜索定位的

在查找c=5的時候,先鎖住了(0,5]。但是因爲c不是唯一索引,爲了確認還有沒有別的記錄c=5,就要向右遍歷,找到c=10確認沒有了,這個過程滿足優化2,所以加了間隙鎖(5,10)。執行c=10會這個邏輯的時候,加鎖的範圍是(5,10]和(10,15),執行c=20這個邏輯的時候,加鎖的範圍是(15,20]和(20,25)

這條語句在索引c上加的三個記錄鎖的順序是:先加c=5的記錄鎖,再加c=10的記錄鎖,最後加c=20的記錄鎖

select id from t where c in(5,20,10) order by c desc for update;

由於語句裏面是order by c desc,這三個記錄鎖的加鎖順序是先鎖c=20,然後c=10,最後是c=5。這兩條語句要加鎖相同的資源,但是加鎖順序相反。當這兩條語句併發執行的時候,就可能出現死鎖

八、insert語句的鎖爲什麼這麼多?

1、insert … select語句

表t和t2的表結構、初始化數據語句如下:

CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(null, 1,1);
insert into t values(null, 2,2);
insert into t values(null, 3,3);
insert into t values(null, 4,4);

create table t2 like t;

在可重複讀隔離級別下,binlog_format=statement時執行下面這個語句時,需要對錶t的所有行和間隙加鎖

insert into t2(c,d) select c,d from t;

2、insert循環寫入

要往表t2中插入一行數據,這一行的c值是表t中c值的最大值加1,SQL語句如下:

insert into t2(c,d)  (select c+1, d from t force index(c) order by c desc limit 1);

這個語句的加鎖範圍,就是表t索引c上的(3,4]和(4,supermum]這兩個next-key lock,以及主鍵索引上id=4這一行

執行流程是從表t中按照索引c倒序嗎,掃描第一行,拿到結果寫入到表t2中,因此整條語句的掃描行數是1

但如果要把這一行的數據插入到表t中的話:

insert into t(c,d)  (select c+1, d from t force index(c) order by c desc limit 1);

在這裏插入圖片描述
explain結果中的Extra字段中Using temporary字段,表示這個語句用到了臨時表

執行流程如下:

1.創建臨時表,表裏有兩個字段c和d

2.按照索引c掃描表t,依次取c=4、3、2、1,然後回表,讀到c和d的值寫入臨時表

3.由於語義裏面有limit 1,所以只取了臨時表的第一行,再插入到表t中

這個語句會導致在表t上做全表掃描,並且會給索引c上的所有間隙都加上共享的next-key lock。所以,這個語句執行期間,其他事務不能在這個表上插入數據

需要臨時表是因爲這類一邊遍歷數據,一邊更新數據的情況,如果讀出來的數據直接寫回原表,就可能在遍歷過程中,讀到剛剛插入的記錄,新插入的記錄如果參與計算邏輯,就跟語義不符

3、insert唯一鍵衝突

在這裏插入圖片描述
sessionA執行的insert語句,發生唯一鍵衝突的時候,並不只是簡單地報錯返回,還在衝突的索引上加了鎖,sessionA持有索引c上的(5,10]共享next-key lock(讀鎖)
在這裏插入圖片描述
在sessionA執行rollback語句回滾的時候,sessionC幾乎同時發現死鎖並返回

1.在T1時刻,啓動sessionA,並執行insert語句,此時在索引c的c=5上加了記錄鎖。這個索引是唯一索引,因此退化爲記錄鎖

2.在T2時刻,sessionA回滾。這時候,sessionB和sessionC都試圖繼續執行插入操作,都要加上寫鎖。兩個session都要等待對方的行鎖,所以就出現了死鎖
在這裏插入圖片描述

4、insert into … on duplicate key update

上面這個例子是主鍵衝突後直接報錯,如果改寫成

insert into t values(11,10,10) on duplicate key update d=100; 

就會給索引c上(5,10]加一個排他的next-key lock(寫鎖)

insert into … on duplicate key update的語義邏輯是,插入一行數據,如果碰到唯一鍵約束,就繼續執行後面的更新語句。如果有多個列違反了唯一性索引,就會按照索引的順序,修改跟第一個索引衝突的行

表t裏面已經有了(1,1,1)和(2,2,2)這兩行,執行這個語句效果如下:
在這裏插入圖片描述
主鍵id是先判斷的,MySQL認爲這個語句跟id=2這一行衝突,所以修改的是id=2的行

思考題:

1、如果要刪除一個表裏面的前10000行數據,有以下三種方法可以做到:

  • 第一種,直接執行delete from T limit 10000;
  • 第二種,在一個連接中循環執行20次delete from T limit 500;
  • 第三種,在20個連接中同時執行delete from T limit 500;

選擇哪一種方式比較好?

參考答案:

第一種方式,單個語句佔用時間長,鎖的時間也比較長,而且大事務還會導致主從延遲

第三種方式,會人爲造成鎖衝突

第二種方式相對較好

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章