超詳細的Mysql鎖 實戰分析,你想知道的都在這裏~

1.mysql回表查詢

在這裏提起主要是用於說明mysql數據和索引的結構,有助於理解後續加鎖過程中的一些問題。

mysql索引結構和表數據結構是相互獨立的,根據索引查詢,只能找到索引列和主鍵聚簇索引。如果select語句中不包含索引列,mysql會根據主鍵聚簇索引二次回表查詢所需要的數據,查詢出來的數據是根據聚簇索引排序的。如果select中只包含索引列和聚簇索引,那麼mysql不會再根據聚簇索引回查表。而且查出來的數據根據查詢索引列是排序的。

需要注意的是,mysql即使沒有聲明主鍵,mysql也會默認根據一個非空列,生成聚簇索引。

我們有以下的表,其中name和age爲索引列,id爲主鍵:

CREATE TABLE `test_user_info` (
  `id` bigint(20) NOT NULL COMMENT '主鍵ID',
  `name` varchar(255) DEFAULT NULL COMMENT '用戶姓名',
  `age` int(11) DEFAULT NULL COMMENT '年齡',
  `salary` decimal(15,2) DEFAULT '100.00' COMMENT '獎金',
  PRIMARY KEY (`id`),
  KEY `index_name` (`name`),
  KEY `index_age` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶信息表'

默認數據如下:

https://pic.imgdb.cn/item/6556e503c458853aef1688d6.png

覆蓋查詢:如果執行SELECT NAME,id FROM my_test.test_user_info WHERE NAME IN ('zhangsan','lisi'),mysql是不會回表查詢的,因爲索引數據結構上已經包含所需要查詢的所有值,且查詢出來的值是有序的。

image-20231117101801689

回表查詢:如果執行SELECT NAME,id,age FROM dongfeng_batch.test_user_info WHERE NAME IN ('zhangsan','lisi'), 由於索引查詢列上無age字段,mysql在根據name索引找到聚簇索引後,還要再根據聚簇索引回表查詢對應的age值,最後數據默認是根據主鍵id排序的。

查詢過程如下:

image-20231113104842964

2.mysql innodb事務隔離級別

查詢mysql事務隔離級別:

SHOW VARIABLES LIKE 'transaction_isolation';

image-20231106155405398

REPEATABLE-READ爲mysql默認的事務隔離級別。

mysql InnoDB引擎下有以下4種隔離級別

隔離級別 髒讀 不可重複讀 幻讀
讀未提交(READ UNCOMMITTED) 出現 出現 出現
讀已提交(READ COMMITTED) 不出現 出現 出現
可重複讀(REPEATABLE READ) 不出現 不出現 可能出現
串行化(SERIALIZABLE) 不出現 不出現 不出現

按照隔離級別水平從低到高排序,隔離級別越高,性能越差。

髒讀 一個事務訪問到了另一個事務未提交的數據 事務A更新了id=1的數據的age爲90但還未提交,事務B這時候去讀id=1的數據,發現age由25變爲90。如果事務A回滾了,事務B讀取到的數據就是無效的。
不可重複讀 一個事務讀取同一條數據2次,得到得結果不一致 事務A讀取id=1的數據,age=25;這時候事務B更新id=1的數據,將age修改爲90並提交。這時候事務A再去讀取,發現age=90,同一事務中對同一條數據讀取的不一致。
幻讀 一個事務讀取2次,得到的數據行數不一致 事務A讀取name=張三的數據,一次讀取出兩條;這時候事務B插入一條name=張三,age=18的數據並提交;事務A再次讀取name=張三的數據,這時候讀取出三條。

3.mysql鎖分類

MySQL鎖家族 (1)

3.1按鎖粒度分類

(1)表(Table Lock)級鎖:表級鎖是對整張表進行鎖定,它是mysql最基本的鎖策略,所有存儲引擎都支持。表鎖開銷最小,粒度最大,發生鎖衝突的概率最高,所以併發度最低,可以很好的避免死鎖問題。

(2)行級鎖:也稱爲記錄鎖。行級鎖是最細粒度的鎖,它只鎖定數據表中的某一行數據。InnoDB引擎才支持行級鎖。行級鎖開銷大,粒度最小,發生鎖衝突概率低,所以併發度最高,容易產生死鎖。

(3)頁級鎖:頁級鎖是最粗粒度的鎖,它鎖定整個數據頁面,即數據庫中存儲數據的最小單位。在使用頁級鎖時,如果有多個用戶想要訪問同一個頁面,則只有一個用戶能夠獲得鎖定,其他用戶要麼等待,要麼被阻塞。頁級鎖也會產生死鎖。BDB引擎支持頁級鎖。

三種級別的鎖分別對應存儲引擎關係如下:

引擎 行鎖 表鎖 頁鎖
MyISAM 支持
InnoDB 支持 支持
BDB 支持 支持

3.1.1表級鎖分類

表級鎖細分爲以下幾種:

表讀鎖(共享鎖-S鎖)及表寫鎖(排他鎖-X鎖)

元數據鎖(meta data lock)

自增鎖(AUTO-INC Locks)

意向鎖(Intention lock),意向鎖又分爲意向共享鎖(intention shared lock,IS):事務有意向對錶中的某些行加共享鎖(S鎖),意向排他鎖(intention exclusive lock,IX):事務有意向對錶中的某些行加排他鎖(X鎖)。

3.1.1.1表讀鎖(共享鎖-S鎖)及表寫鎖(排他鎖-X鎖)

一般在MyISAM引擎中會用到,MyISAM引擎不支持行鎖。InnoDB也支持,但是一般需要手動獲取。

在對某個表執行SELECT、INSERT、DELETE、UPDATE語句時,InnoDB存儲引擎是不會爲這個表添加表級別的S鎖或者X鎖的。在對某個表執行一些諸如ALTER TABLE、DROP TABLE這類的DDL語句時,其他事務對這個表併發執行諸如SELECT、INSERT、DELETE、UPDATE的語句會發生阻塞。同理,某個事務中對某個表執行SELECT、INSERT、DELETE、UPDATE語句時,在其他會話中對這個表執行DDL語句會發生阻塞。這個過程其實是通過在server層使用一種稱之爲元數據鎖(英文名:Metadata Locks,簡稱MDL)結構來實現的。

需要注意的是,在SERIALIZABLE, 任何查詢都是會加讀鎖的。

MyISAM引擎在對錶做查詢的時候不會顯式地加鎖,當一個查詢需要修改表中的數據(寫操作)時,會加上寫鎖,這會阻塞其他的寫操作和讀操作,直到該寫操作完成。

一般情況下,不會使用InnoDB存儲引擎提供的表級別的S鎖和X鎖。只會在一些特殊情況下,比方說崩潰恢復過程中用到。比如,在系統變量autocommit=0,innodb_table_locks=1時,手動獲取InnoDB存儲引擎提供的表t的S鎖或者X鎖可以這麼寫:
LOCK TABLES t READ:InnoDB存儲引擎會對錶t加表級別的S鎖。
LOCK TABLES t WRITE:InnoDB存儲引擎會對錶t加表級別的X鎖。

3.1.1.2意向鎖(Intention lock)

InnoDB支持,MyISAM不支持

上面說到,InnoDB存儲引擎在做SELECT、INSERT、DELETE、UPDATE操作的時候,不會爲表加上S鎖或者X鎖的,但是會使用到意向鎖這種表級別鎖。MyISAM引擎是不支持意向鎖的。

意向鎖又分爲意向共享鎖(intention shared lock,IS):事務有意向對錶中的某些行加共享鎖(S鎖);意向排他鎖(intention exclusive lock,IX):事務有意向對錶中的某些行加排他鎖(X鎖)。事務在給一個數據行加共享鎖前必須取得該表的IS鎖;事務在給一個數據行加排他鎖前必須取得該表的IX鎖。意向鎖的引入主要是爲了在進行行級鎖或頁級鎖時,提供一種機制來表示事務可能會對錶中的某些行或頁面進行鎖定操作的意向,從而提高併發控制的效率。

需要注意的是,意向鎖是MySQL內部自動管理的,通常不需要顯式地進行操作。在實際應用中,開發人員只需瞭解意向鎖的概念,而無需直接干預它們的使用。 MySQL會在需要時自動處理意向鎖,以確保事務的併發操作能夠正確進行

InnoDB支持多粒度鎖(multiple granularity locking),它允許行級鎖與表級鎖共存,而意向鎖就是其中的一種表鎖。
1、意向鎖的存在是爲了協調行鎖和表鎖的關係,支持多粒度(表鎖與行鎖)的鎖並存。
2、意向鎖是一種不與行級鎖衝突表級鎖,這一點非常重要。
3、表明“某個事務正在某些行持有了鎖或該事務準備去持有鎖”。

如果沒有意向寫鎖,mysql在加行鎖之前,需要循環掃描表,判斷表是否有行鎖。如果有了意向寫鎖,這樣,mysql在加行寫鎖時,如果判斷表上沒有意向寫鎖,可以直接加行寫鎖,無需掃描。

事務要獲取某些行的S鎖,必須先獲得表的IS鎖
SELECT column FROM table ... LOCK IN SHARE MODE;

事務要獲取某些行的X鎖,必須先獲得表的IX鎖。
SELECT column FROM table ... FOR UPDATE;

3.1.1.3自增鎖(AUTO-INC鎖)

InnoDB支持,MyISAM不支持

自增鎖通常是指對自增主鍵列的併發控制,這就要求我們設計表的時候,添加了一列做爲自增主鍵纔會使用到。在使用MyISAM存儲引擎時,對於自增主鍵列的併發控制並不是通過鎖來實現的,而是通過維護一個全局計數器來實現的。MyISAM存儲引擎中的自增字段是通過維護一個計數器來生成新的自增值,而不是通過加鎖的方式來保證唯一性。這也意味着在高併發的情況下,可能會出現自增值的重複或者不連續的情況。相比之下,InnoDB存儲引擎支持的自增字段會通過鎖機制來保證併發插入時自增值的唯一性和連續性,從而避免了在高併發情況下可能出現的問題。

在數據庫中,插入數據的方式可以總結爲三種:

  1. 簡單插入(Simple Inserts):可以預先確定要插入的行數(當語句被初始處理時)的語句。包括沒有嵌套子查詢的單行和多行INSERT ... VALUES()REPLACE語句。。
  2. 批量插入(Bulk Inserts):事先不知道要插入的行數(和所需自動遞增值的數量)的語句。比如INSERT ... SELECTREPLACE ... SELECTLOAD DATA語句,但不包括純INSERT。InnoDB在每處理一行,爲AUTO_INCREMENT列分配一個新值。
  3. 混合模式插入(Mixed-mode Inserts):這些是“Simple inserts”語句但是指定部分新行的自動遞增值。例如INSERT INTO teacher (id,name) VALUES (1,'a'),(NULL,'b'),(5,'c'),(NULL,'d');只是指定了部分id的值。另一種類型的“混合模式插入”是INSERT ... ON DUPLICATE KEY UPDATE。

對於上面數據插入的案例,MySQL中採用了自增鎖的方式來實現,AUTO-INC鎖是當向使用含有AUTO_INCREMENT列的表中插入數據時需要獲取的一種特殊的表級鎖,在執行插入語句時就在表級別加一個AUTO-INC鎖,然後爲每條待插入記錄的AUTO_INCREMENT修飾的列分配遞增的值,在該語句執行結束後,再把AUTO-INC鎖釋放掉。一個事務在持有AUTO-INC鎖的過程中,其他事務的插入語句都要被阻塞,可以保證一個語句中分配的遞增值是連續的。也正因爲此,其併發性顯然並不高,當我們向一個由AUTO_INCREMENT關鍵字的主鍵插入值的時候,每條語句都要對這個表鎖進行競爭,這樣的併發潛力其實是很低下的,所以innodb通過innodb_autoinc_lock_mode的不同取值來提供不同的鎖定機制,來顯著提高SQL語句的可伸縮性和性能。

我們可以使用**SHOW VARIABLES LIKE '%innodb_autoinc_lock_mode%'**命令查詢自增鎖的鎖定模式。

image-20231108103939203

innodb_autoinc_lock_mode有三種取值,分別對應與不同鎖定模式:
(1)innodb_autoinc_lock_mode = 0(“傳統”鎖定模式)
在此鎖定模式下,所有類型的insert語句都會獲得一個特殊的表級AUTO-INC鎖,用於插入具有AUTO_INCREMENT列的表。這種模式其實就如我們上面的例子,即每當執行insert的時候,都會得到一個表級鎖(AUTO-INC鎖),使得語句中生成的auto_increment爲順序,且在binlog中重放的時候,可以保證master與slave中數據的auto_increment是相同的。因爲表級鎖,當在同一時間多個事務中執行insert的時候,對於AUTO-INC鎖的爭奪會限制併發能力。

(2)innodb_autoinc_lock_mode = 1(“連續”鎖定模式)
在MySQL8.0之前,連續鎖定模式是默認的。
在這個模式下,“bulk inserts”仍然使用AUTO-INC表級鎖,並保持語句結束。這適用於所有INSERT … SELECT,REPLACE … SELECT和LOAD DATA語句。同一時刻只有一個語句可以持有AUTO-INC鎖。
對於“Simple inserts”(要插入的行數事先已知),則通過mutex(輕量鎖)的控制下獲得所需數量的自動遞增值來避免表級AUTO-INC鎖,它只在分配過程的持續時間內保持,而不是直到語句完成。不使用表級AUTO-INC鎖,除非AUTO-INC鎖由另一個事務保持。如果另一個事務保持AUTO-INC鎖,則“Simple inserts”等待AUTO-INC鎖。

(3)innodb_autoinc_lock_mode = 2(“交錯”鎖定模式)
從MySQL8.0開始,交錯模式是默認設置。
在這種鎖定模式下,所有類INSERT語句都不會使用表級AUTO-INC鎖,而是使用較爲輕量的mutex鎖,並且可以同時執行多個語句。這是最快和最可擴展的鎖定模式。副作用就是單個Insert的自增值有可能是不連續的,因爲AUTO_INCREMENT的值會在多個INSERT語句中來回交叉執行。

優點:效率高;

缺點:在併發情況下無法保持數據一致性。我們知道mysql通過Binlog主從同步有三種模式:statement,row,mixed;如果採用的是statement模式同步的數據,並且採用了交錯鎖定模式,數據會有不一致問題。

3.1.1.4元數據鎖(meta data lock)

innodb支持,MyISAM不支持

MySQL5.5引入了meta data lock,簡稱MDL鎖,屬於表鎖範疇。MDL的作用是,保證讀寫的正確性。比如,如果一個查詢正在遍歷一個表中的數據,而執行期間另一個線程對這個表結構做變更,增加了一列,那麼查詢線程拿到的結果跟表結構對不上,肯定是不行的。
因此,當對一個表做增刪改查操作的時候,加MDL讀鎖;當要對錶做結構變更操作的時候,加MDL寫鎖。
讀鎖之間不互斥,因此你可以有多個線程同時對一張表增刪改查。讀寫鎖之間、寫鎖之間是互斥的,用來保證變更表結構操作的安全性,解決了DML和DDL操作之間的一致性問題。不需要顯式使用,在訪問一個表的時候會被自動加上。

思考:我們在對錶做Alter操作的時候,是否能立即執行?

答案是不一定,如果此時還有事務在進行增刪改查操作,Alter操作會阻塞,必須等待所有事務執行完畢才能執行。

需要注意的是,我們在對大表做DDL的時候,有可能會造成數據庫崩潰。所以要求我們儘量在業務不繁忙的時候執行DDL,或者是使用第三方工具,如pt-online-schema-change等來安全的執行表的DDL操作。

3.1.1.5表級鎖兼容性

IS IX AUTO-INC S X
IS 兼容 兼容 兼容 兼容 不兼容
IX 兼容 兼容 兼容 不兼容 不兼容
AUTO-INC 兼容 兼容 不兼容 不兼容 不兼容
S 兼容 不兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容 不兼容

3.1.2行級鎖分類

InnoDB引擎支持行級鎖,行級鎖只在存儲引擎層實現。

優點:鎖粒度小,發生鎖衝突概率低,併發度高;

缺點:鎖開銷較大,加鎖較慢,容易出現死鎖。

3.1.2.1記錄鎖(Record Locks)

在 MySQL 中,記錄鎖可以分爲共享鎖(S鎖)和排他鎖(X鎖)。共享鎖允許多個事務同時對同一行進行讀取操作,但是不允許任何事務對該行進行寫入操作;而排他鎖則會阻止其他事務對該行進行讀取或寫入操作,直到持有排他鎖的事務釋放鎖定。記錄鎖始終鎖定索引記錄,即使表沒有定義索引。對於這種情況,InnoDB會創建一個隱藏的聚簇索引並使用這個索引進行記錄鎖定。

當事務需要修改數據時,會根據需要獲取相應的鎖,以確保併發修改操作的正確性和一致性。這些鎖會在事務提交或回滾時自動釋放。

記錄鎖也就是僅僅一條記錄鎖上,官方的類型名稱爲:LOCK_REC_NOT GAP

當一個事務獲取了一條記錄的S型鎖後,其他事務也可以繼續獲取該記錄的S型記錄鎖,但不可以繼續獲取X型記錄鎖;
當一個事務獲取了一條記錄的X型記錄鎖後,其他事務即不可以繼續獲取該記錄的S型記錄鎖,也不可以繼續獲取X型記錄鎖。

3.1.2.2間隙鎖(Gap Locks)

間隙鎖(Gap Lock)是InnoDB引擎RR隔離級別下特有的鎖機制,用於防止幻讀(Phantom Reads),但不能完全避免幻讀。間隙鎖鎖定的是索引記錄之間的間隙,或者在第一個索引記錄之前或最後一個索引記錄之後的間隙。間隙鎖只會阻塞insert操作。

當多個事務執行範圍查詢並且涉及相同的間隙時,它們可以共享同一個間隙鎖。這意味着這些事務不會互相阻塞,它們可以同時持有同一個間隙鎖,並且可以讀取該間隙範圍內的數據。這種情況下的共享間隙鎖可以提高併發性能。此外,即使多個事務可以共享同一個間隙鎖,它們之間仍然可能存在衝突。例如,如果一個事務持有了間隙鎖並嘗試插入新的鍵值,而另一個事務持有了間隙鎖並嘗試在同一間隙內插入另一個新的鍵值,那麼它們之間仍然會發生衝突,其中一個事務將被阻塞。

然而,多個事務無法同時持有相互重疊的間隙鎖。這可能會導致一些事務被阻塞,直到其他事務釋放了鎖。

3.1.2.3臨鍵鎖(Next-Key Locks)

MySQL的臨鍵鎖(next-key lock)是一種加強版的間隙鎖,用於解決間隙鎖無法完全避免的幻讀問題。臨鍵鎖不僅會鎖定範圍內的間隙,還會鎖定範圍內的已有記錄。所以InnoDB就提出了一種稱之爲Next-Key Locks的鎖,官方的類型名稱爲:LOCK_ORDINARY,我們也可以簡稱爲net-key鎖。Next-Key Locks是存儲引擎innodb、事務級別在RR(可重複讀)的情況下使用的數據庫鎖,innodb默認的鎖就是Next-Key locks

類似是一個記錄鎖和一個gap鎖的合體。gap鎖是允許多個事務持有相同間隙的,但臨鍵鎖是不允許多個事務共同持有的。臨鍵鎖鎖住的間隙是不允許其他事務操作的。它即能保護該條記錄,又能阻止別的事務將新紀錄插入被保護紀錄的間隙

臨建鎖遵循左開右閉的原則。

需要注意的是,臨建鎖會退化。

退化規則如下:如果是RC隔離級別,是沒有間隙鎖的,只會有行鎖。

查詢描述 查詢值是否存在 加鎖情況
非唯一索引等值查詢 查詢值存在 加next-key Lock和間隙鎖
非唯一索引等值查詢 查詢值不存在 加next-key Lock,後退化爲間隙鎖
非唯一索引範圍查詢 不管值是否存在 加next-key Lock,可能數據行加記錄鎖
唯一索引等值查詢 查詢值存在 加next-key Lock,但會退化爲記錄鎖
唯一索引等值查詢 查詢值不存在 加next-key Lock,但會退化爲間隙鎖
唯一索引範圍查詢 不管值是否存在 加next-key Lock,可能會退化爲間隙鎖/記錄鎖
非索引列查詢 不管值是否存在 全表主鍵範圍內加臨鍵鎖

臨建鎖加鎖過程是一個比較複雜的過程,後面在mysql加鎖分析中舉例論證。

3.1.2.4插入意向鎖(Insert Intention Locks)

插入意向鎖是一種在 INSERT 操作之前設置的一種間隙鎖,插入意向鎖表示了一種插入意圖,即當多個不同的事務,同時往同一個索引的同一個間隙中插入數據的時候,它們互相之間無需等待,即不會阻塞(要是單純按照之前間隙鎖的理論,必須要等一個間隙鎖釋放了,下一個事務纔可以往相同的間隙處插入數據)。假設有值爲 4 和 7 的索引記錄,現在有兩個事務,分別嘗試插入值爲 5 和 6 的記錄,在獲得插入行的排他鎖之前,每個事務使用插入意向鎖鎖定 4 和 7 之間的間隙,但是這兩個事務不會相互阻塞,因爲行是不衝突的。

這就是插入意向鎖。插入意向鎖和臨建鎖和間隙鎖是互斥的。

3.1.2.5空間索引謂詞鎖(Predicate Locks for Spatial Indexes)

MySQL的空間索引用於處理地理空間數據,允許對存儲地理位置信息的列進行高效的空間查詢。通過空間索引,可以加快對地理空間數據的搜索、分析和操作。

在MySQL中,空間索引通常用於存儲地理位置座標、多邊形區域等地理空間數據。它可以支持多種空間函數,如計算兩點之間的距離、查找某一點所在的區域等操作。

要創建空間索引,需要使用特定的索引類型,例如SPATIAL索引。下面是一個創建空間索引的示例:

CREATE TABLE spatial_table (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(100),
    location POINT,
    SPATIAL INDEX(location)
);

在這個示例中,我們創建了一個名爲spatial_table的表,其中包含一個用於存儲地理位置的location列。通過使用SPATIAL INDEX關鍵字,我們爲location列創建了一個空間索引。

一旦創建了空間索引,就可以使用MySQL的空間函數來執行各種地理空間數據的查詢和分析操作。例如,可以使用ST_Distance函數計算兩個地理位置之間的距離,使用ST_Contains函數檢查一個區域是否包含另一個區域,等等。

總之,MySQL的空間索引爲處理地理空間數據提供了便利和高效性,使得開發地理信息系統和地圖應用變得更加容易和高效。

空間索引的謂詞鎖實際上是一種行級鎖。當使用空間索引進行查詢時,InnoDB會在涉及到的行上設置謂詞鎖,以確保其他事務不能插入或修改與查詢條件匹配的行。這樣可以保證空間數據的一致性和完整性。因此,謂詞鎖可以看作是對行級操作的一種鎖定機制。

4.mysql加鎖分析

文章結合mysql8.0+版本(8.0.35)的performance_schema.data_locks分析。

當前mysql5.7.30的information_schema.INNODB_LOCKS表不夠直觀,不能很輕鬆的看出加鎖情況。

mysql8.0+和mysql5.7.30在加鎖情況上沒有較大差異。後續所有操作都是基於mysql8.0.35。

備註:

mysql5.7.30要打開information_schema.INNODB_LOCKS的功能,需開啓以下配置:

#開啓innodb鎖功能
SET GLOBAL innodb_status_output_locks = ON;

#查詢是否開啓
SHOW GLOBAL VARIABLES LIKE 'innodb_status_output_locks';

正常查詢不涉及行鎖,這裏不分析查詢的情況。數據庫默認使用RR隔離級別。

RR隔離級別下,對如下數據分析,name和age分別爲普通索引,id爲主鍵

數據說明

注意插入的順序,我這裏打亂了id=68和id=59的順序。mysql查詢鎖日誌默認是按數據庫插入順序排序的,但是並不代表加鎖的順序。

insert into `test_user_info` (`id`, `name`, `age`, `salary`) values('12','lisi','12','100.00');
insert into `test_user_info` (`id`, `name`, `age`, `salary`) values('35','wangwu','49','100.00');
insert into `test_user_info` (`id`, `name`, `age`, `salary`) values('47','zhangsan','13','100.00');
insert into `test_user_info` (`id`, `name`, `age`, `salary`) values('68','zhaojiu','25','100.00');
insert into `test_user_info` (`id`, `name`, `age`, `salary`) values('59','qianliu','43','100.00');

image-20231114113644445

performance_schema.data_locks表說明:

  • 如果 LOCK_MODE 爲 X, REC_NOT_GAP,說明是記錄鎖;
  • 如果 LOCK_MODE 爲 X, GAP,說明是間隙鎖;
  • 如果LOCK_MODE 爲 X,說明是 next-key 鎖;

總結:(這是非常重要的結論,否則沒法直觀的通過performance_schema.data_locks表分析出加鎖情況)

1.LOCK_MODE=X,GAP類型,LOCK_DATA是間隙鎖的結束範圍,從該行數據向上掃描,鄰近的一行則是間隙鎖的開始範圍;

2.LOCK_MODE=X類型,LOCK_DATA是臨鍵鎖的尾,從該行向上掃描,鄰近的一行則是臨鍵鎖的開始範圍;如果表中沒有臨近的數據行,則是無窮小

3.LOCK_DATA=supremum pseudo-record,這個是無窮大的意思

4.非唯一索引記錄上加臨鍵鎖,會在對應數據行上加記錄鎖

5.不管是臨鍵鎖還是間隙鎖,會根據查詢條件向上向下延伸,找到不滿足條件的臨近的行;

下面的情況在分析的時候,結合總結來分析。

場景一:唯一索引等值查詢,查詢數據不存在

結論:加間隙鎖

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where id = 3

查詢數據不存在,查詢加鎖情況:

注意:以下返回值的順序不代表加鎖順序

image-20231114114642411

image-20231114113644445

我們發現:

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,GAP,INDEX_NAME=PRIMARY,說明在主鍵上被加了間隙鎖。

間隙鎖範圍id爲(-∞,12)。爲什麼是(-∞,12)。因爲加鎖的時候,爲了防止幻讀,鎖住對應的間隙,臨界範圍會根據查詢條件在數據庫中找不滿足查詢條件的相鄰的值。如果向上向下找不到不滿足條件的值,則是-∞和+∞。

mysql innodb引擎在RR級別下默認加的是臨鍵鎖。在當前場景下,臨鍵鎖會退化爲間隙鎖。

3.如果其他事務SessionB想在id < 12的範圍內插入數據的話,事務會被阻塞。只有等待SessionA提交後才能執行。

image-20231114122842478

場景二:唯一索引等值查詢,查詢數據存在

結論:加行鎖

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where id = 12

查詢數據存在,查看加鎖情況:

image-20231114123549834

image-20231114113644445

我們發現:

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,說明在數據行上被加了記錄鎖。

記錄鎖鎖定id=12的記錄。mysql innodb引擎在RR級別下默認加的是臨鍵鎖,在這種場景下,臨鍵鎖會退化爲記錄鎖。

這時SessionB立刻插入id=5的數據,是不會被阻塞的。

image-20231114123921563

場景三:唯一索引>=範圍查詢,等值查詢數據存在

結論:加行鎖和間隙鎖;

SessionA執行以下sql,不提交事務:

update test_user_info set salary = salary + 100 where id >= 59

id=59的數據存在,我們查詢加鎖情況:

image-20231114124431735

image-20231114113644445

此時加鎖情況已經較爲複雜,我們發現:

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,說明在數據行id=59上被加了記錄鎖。

3.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=PRIMARY,LOCK_DATA=supremum pseudo-record,這個是無窮大的意思。說明id範圍在(68, +無窮大]的數據上被加了臨鍵鎖。

4.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=PRIMARY,LOCK_DATA=68。說明在(59, 68]的數據上被加了臨鍵鎖。

臨鍵鎖遵循左開右閉的原則。當查詢數據範圍爲id>=59的時候,mysql掃描到id=59行的數據,數據存在,加行鎖。然後再掃描id > 59的情況,掃描到id=68的數據,所以在(59,68]上加臨鍵鎖。然後繼續掃描id>68的情況,此時表裏面沒有數據,所以在(68, +無窮大]的數據上被加了臨鍵鎖。

我們講過,臨建鎖類似記錄鎖+間隙鎖的組合,所以這時候,如果我們SessionB執行:

update test_user_info set salary = salary + 100 where id = 68,此時68的數據行上有臨鍵鎖,事務會被阻塞。

我們想在id >=59的範圍內插入數據,也是會被阻塞的。

image-20231114125422011

場景四:唯一索引>=範圍查詢,等值查詢數據不存在,這種就類似於>範圍查詢

結論:加臨鍵鎖

SessionA執行以下sql,不提交事務:

update test_user_info set salary = salary + 100 where id >= 58

id=58的數據不存在,這種類似於>58的查詢,查詢加鎖情況。

image-20231114130058830

image-20231114113644445

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=PRIMARY,LOCK_DATA=supremum pseudo-record,這個是無窮大的意思。說明id範圍在(68, +無窮大]的數據上被加了臨鍵鎖。

3.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=PRIMARY,LOCK_DATA=68。說明在(59, 68]的數據上被加了臨鍵鎖。

4.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=PRIMARY,LOCK_DATA=59。說明在(47, 59]的數據上被加了臨鍵鎖。爲什麼這裏是47,因爲加鎖的時候,爲了防止幻讀,鎖住對應間隙,臨界條件會根據查詢條件在數據庫中找不滿足查詢條件的相鄰的值。我們這裏id >= 58, 相鄰的不滿足條件的值爲id=47。

總結就是:唯一索引上的範圍查詢會訪問到不滿足條件的第一個值爲止。

如果我們SessionB執行:

insert into my_test.test_user_info values (48, 'pengwu', 30, 120)會被阻塞;

image-20231114131813987

但是如果執行的插入數據id=46,則

可以執行,不被阻塞。

image-20231114131934790

場景五:唯一索引<=範圍查詢,等值查詢數據存在

結論:只加臨鍵鎖;

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where id <= 35

id=35的數據存在,我們查詢加鎖情況:

image-20231114132902711

![image-20231114113644445(https://pic.imgdb.cn/item/6556f082c458853aef358304.png)

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=PRIMARY,LOCK_DATA=12。說明id範圍在(-∞, 12]的數據上被加了臨鍵鎖。

3.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=PRIMARY,LOCK_DATA=35。說明在(12, 35]的數據上被加了臨鍵鎖。

這裏和>=範圍查詢,等值數據存在場景不一樣的是,等值查詢id=35的數據不會加行鎖。這是因爲臨鍵鎖左開右閉的原則。(12, 35]的臨鍵鎖範圍已經包含了數據id=35的行。

如果此時SessionB想在id範圍爲:id<=35的行上做任何操作,insert,update,delete,哪怕where條件是其他列,但操作的行數據id<=35, 均被阻塞。

SessionB更新:update my_test.test_user_info set salary = salary + 100 where name = 'lisi'

數據id=12,被阻塞。

image-20231114134028391

場景六:唯一索引<=範圍查詢,等值查詢數據不存在,這種就類似於<範圍查詢

結論:滿足條件的範圍加臨鍵鎖,不滿足條件的加間隙鎖;

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where id < 35

我們查詢加鎖情況:

hahah

image-20231114113644445

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=PRIMARY,LOCK_DATA=12。說明id範圍在(-∞, 12]的數據上被加了臨鍵鎖。

3.LOCK_TYPE=RECORD,LOCK_MODE=X,GAP,INDEX_NAME=PRIMARY,LOCK_DATA=35。說明在(12, 35)的數據上被加了間隙鎖。這裏爲什麼變成了間隙鎖,因爲加鎖的時候,爲了防止幻讀,鎖住對應的間隙,臨界範圍會根據查詢條件在數據庫中找不滿足查詢條件的相鄰的值。id=35則是向下不滿足條件的相鄰的值。mysql開始在

id爲(12,35]的行上加臨鍵鎖。但是id=35的數據在範圍外,退化成了間隙鎖。爲什麼這裏這麼特殊,我想的是,id=35的數據不在範圍內,沒必要加上行鎖,所以這裏就退化成間隙鎖。保證id=35行的數據可以被操作。這個和>=範圍的完全不同。

總結:<=的範圍查詢,如果查詢條件不在臨界值,臨鍵範圍是會退化成間隙鎖的。

SessionB此時是可以操作id=35的行數據的。

場景七:唯一索引>= and <=範圍查詢

這種基本是場景三,四,五,六的組合,沒有什麼特殊,即使等值查詢數據存在。

SessionA執行以下sql,不提交事務:

update test_user_info set salary = salary + 100 where id >= 12 and id <= 38

我們查詢加鎖情況:

image-20231114141354638

image-20231114113644445

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=12。說明id=12的數據行加了記錄鎖。

3.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=PRIMARY,LOCK_DATA=35,說明id範圍爲(12,35]的數據行上加了臨鍵鎖。

4.LOCK_TYPE=RECORD,LOCK_MODE=X,GAP,INDEX_NAME=PRIMARY,LOCK_DATA=47,說明id範圍爲(35,47)的行上加了間隙鎖。

場景八:非唯一索引等值查詢,查詢數據不存在

結論:加間隙鎖;

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where age = 15

我們查詢加鎖情況:

image-20231114142923256

image-20231114143312985

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,GAP,INDEX_NAME=index_age,LOCK_DATA=25, 68。(25, 68)對應age=25,id=68的索引記錄。注意,mysql索引是有序的。

說明在(age,id)範圍爲:(13,47)到(25,68)的索引數據上加了間隙鎖。那麼對於age=13或age=25的數據,能不能插入進去呢?

這還要根據id範圍確定。實際上,間隙鎖是加在了(13,47)和(25,68)的範圍。

如果我們此時要插入age=13,id=46的數據,是可以插入的。

但是我們要插入age=13, id=48的數據,就會被阻塞。

image-20231114161448615

image-20231114161512239

場景九:非唯一索引等值查詢,查詢數據存在

結論:非唯一索引加臨鍵鎖,間隙鎖;數據行加記錄鎖

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where age = 13

我們查詢加鎖情況:

image-20231114154659488

image-20231114143312985

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(13,47)。說明

臨鍵鎖加在了age和id範圍在(12,12)到[13,47]之間。此時如果SessionB插入(age,id)=(12,11)的數據,是不會阻塞的。

3.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=47,說明id=47的數據行上加了記錄鎖。

4.LOCK_TYPE=RECORD,LOCK_MODE=X,GAP,INDEX_NAME=index_age,LOCK_DATA=(25,68),說明(age,id)範圍在(13,47)到(25,68)範圍內被加上了間隙鎖。

場景十:非唯一索引>=範圍查詢,等值查詢數據存在

結論:非唯一索引記錄加臨鍵鎖,數據行加行鎖。

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where age >= 43

age=43的記錄存在,我們查詢加鎖情況:

image-20231114165057005

image-20231114143312985

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(supremum pseudo-record)。說明(age,id)範圍在(49,35)到[age無窮大,id無窮大]的索引記錄上加了臨鍵鎖;SessionB在比(49,35)

範圍大的記錄上,是無法執行插入操作的。

3.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(49,35)。說明(age,id)範圍在(43,59)到[49,35]的索引記錄上加了臨鍵鎖;

4.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(43,59)。說明(age,id)範圍在(25,68)到[43,59]的索引記錄上加了臨鍵鎖;

5.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=35。說明

id=35的行被加了記錄鎖;

6.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=59。說明

id=59的行被加了記錄鎖;

場景十一:非唯一索引>=範圍查詢,等值查詢數據不存在,這種就類似於>範圍查詢

非常類似場景十

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where age > 43

age=43的記錄不存在,我們查詢加鎖情況:

image-20231114170648397

image-20231114143312985

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(supremum pseudo-record)。說明(age,id)範圍在(49,35)到[age無窮大,id無窮大]的索引記錄上加了臨鍵鎖;SessionB在比(49,35)

範圍大的記錄上,是無法執行插入操作的。

3.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(49,35)。說明(age,id)範圍在(43,59)到[49,35]的索引記錄上加了臨鍵鎖;

4.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=35。說明

id=35的行被加了記錄鎖;

場景十二:非唯一索引<=範圍查詢,等值查詢數據存在

結論:非唯一索引加臨鍵鎖,數據行加行鎖

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where age <= 13

age=13的記錄存在,我們查詢加鎖情況:

image-20231114171302512

image-20231114143312985

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(12,12)。說明(age,id)範圍在(age無窮小,id無窮小)到[12,12]的索引記錄上加了臨鍵鎖;

3.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(13,47)。說明(age,id)範圍在(12,12)到[13,47]的索引記錄上加了臨鍵鎖;

3.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(25,68]。說明(age,id)範圍在(13,47)到[25,68]的索引記錄上加了臨鍵鎖;

4.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=12。說明

id=12的行被加了記錄鎖;

5.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=47。說明

id=47的行被加了記錄鎖;

6.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=68。說明

id=68的行被加了記錄鎖;((25,68]的索引被加了臨鍵鎖,id對應的數據行需要加記錄鎖)

場景十三:非唯一索引<=範圍查詢,等值查詢數據不存在,這種就類似於<範圍查詢

結論:非唯一索引加臨鍵鎖,數據行加行鎖

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where age < 14

age=14不存在,我們查詢加鎖情況,加鎖情況完全同場景十二:

image-20231114172459835

image-20231114143312985

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(12,12)。說明(age,id)範圍在(age無窮小,id無窮小)到[12,12]的索引記錄上加了臨鍵鎖;

3.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(13,47)。說明(age,id)範圍在(12,12)到[13,47]的索引記錄上加了臨鍵鎖;

3.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(25,68]。說明(age,id)範圍在(13,47)到[25,68]的索引記錄上加了臨鍵鎖;

4.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=12。說明

id=12的行被加了記錄鎖;

5.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=47。說明

id=47的行被加了記錄鎖;

6.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=68。說明

id=68的行被加了記錄鎖;(這個不明白爲什麼要在id=68的行數據上加記錄,可能是bug)

場景十四:非唯一索引>= and <=範圍查詢

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where age >=13 and age <= 44

查詢加鎖情況:

image-20231114173507677

image-20231114143312985

1.LOCK_TYPE=TABLE,LOCK_MODE=IX,說明在表上被加了IX意向鎖;

2.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(49,35)。說明(age,id)範圍在(43,59)到[49,35]的索引記錄上加了臨鍵鎖;

3.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(13,47)。說明(age,id)範圍在(12,12)到[13,47]的索引記錄上加了臨鍵鎖;

4.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(25,68)。說明(age,id)範圍在(13,47)到[25,68]的索引記錄上加了臨鍵鎖;

5.LOCK_TYPE=RECORD,LOCK_MODE=X,INDEX_NAME=index_age,LOCK_DATA=(43,59)。說明(age,id)範圍在(25,68)到[43,59]的索引記錄上加了臨鍵鎖;

6.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=35。說明

id=35的行被加了記錄鎖;

7.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=47。說明

id=47的行被加了記錄鎖;

8.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=68。說明

id=68的行被加了記錄鎖;

9.LOCK_TYPE=RECORD,LOCK_MODE=X,REC_NOT_GAP,INDEX_NAME=PRIMARY,LOCK_DATA=59。說明

id=59的行被加了記錄鎖;

場景十五:非索引列查詢

SessionA執行以下sql,不提交事務:

update my_test.test_user_info set salary = salary + 100 where salary = 100

查看加鎖情況:

image-20231114175524394

全表掃描,所有數據行id間的範圍都加上了臨鍵鎖。

(-∞,12],(12,35],(35,47],(47,59],(59,68],(68,+∞], 相當於給表加了X鎖。任何insert,update,delete操作均會被阻塞。

如果是多條件列,mysql會根據每個條件掃描到到的行,來判斷加鎖情況。

6.InnoDB死鎖

在mysql中,死鎖是不會發生在MyISAM引擎中。這是因爲主要使用的是表鎖,而InnoDB中重點使用的是行鎖。

死鎖產生的四個條件:

1.互斥條件:一個資源每次只能被一個進程使用;

2.請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放;

3.不可剝奪條件:進程已獲得的資源,在沒使用完成之前,不能強行剝奪;

4.循環等待條件:多個進程之間形成了一種相互循環等待資源的關係;

結合mysql,我們如何避免死鎖呢?

1.加鎖順序一致,主要打破循環等待條件;

2.儘量基於主鍵或唯一索引更新數據;

5.1表級死鎖產生場景

用戶A開啓事務,首先使用非索引列更新表t1,由於表t1沒有使用索引,導致t1相當於被加上表鎖;

用戶B開啓事務,首先使用非索引列更新表t2,由於表t2沒有使用索引,導致t2相當於被加上表鎖;

這時用戶A去更新表t2,用戶B去更新表t1。用戶A需要等待用戶B釋放表t2的表鎖,用戶B需要用戶A

釋放表t1的表鎖。

這種死鎖場景不常見,我們只要在程序中,保證表的更新順序即可。

5.2行級鎖死鎖產生場景

mysql因爲行級鎖產生的死鎖情況較多,下面簡單列舉幾種情況。

場景1:id主鍵

sessionA:

update test_user_info set age =10 where id = 1;(執行時序1)

update test_user_info set age =10 where id = 12;(執行時序3)

sessionB:

update test_user_info set age =20 where id = 12;(執行時序2)

update test_user_info set age =20 where id= 1;(執行時序4)

這種場景容易產生死鎖,我們在同一個事務中,要儘可能做到同表的某些數據一次性更新;

按照id進行排序,然後按序處理;

場景2:假設一個表有以下數據:id主鍵,name索引,age索引

CREATE TABLE `test_user_info` (
 `id` bigint(20) NOT NULL COMMENT '主鍵ID',
 `name` varchar(255) DEFAULT NULL COMMENT '用戶姓名',
 `age` int(11) DEFAULT NULL COMMENT '年齡',
 `salary` decimal(15,2) DEFAULT '100.00' COMMENT '獎金',
 PRIMARY KEY (`id`),
 KEY `index_name` (`name`),
 KEY `index_age` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶信息表';

insert into `test_user_info` (`id`, `name`, `age`, `salary`) values('1','zhangsan','15','100.00');
insert into `test_user_info` (`id`, `name`, `age`, `salary`) values('12','lisi','12','100.00');
insert into `test_user_info` (`id`, `name`, `age`, `salary`) values('35','wangwu','49','200.00');
insert into `test_user_info` (`id`, `name`, `age`, `salary`) values('47','zhangsan','13','100.00');
insert into `test_user_info` (`id`, `name`, `age`, `salary`) values('59','qianliu','43','200.00');
insert into `test_user_info` (`id`, `name`, `age`, `salary`) values('68','zhaojiu','25','200.00');

sessionA執行以下sql未提交:update test_user_info set salary=salary+100 where name = 'zhangsan';

主表記錄鎖加鎖順序按id排序如下:1,47

image-20231115140530000

sessionB執行以下sql未提交:update test_user_info set salary=salary+120 where age > 12 and age < 25;

主表記錄鎖加鎖順序按id排序如下:47,1

image-20231115140550780

主表記錄鎖順序不一致,產生死鎖。但這個死鎖還有一個極端條件,加鎖過程中,sessionA先對id=1的數據行加鎖,

sessionB再對id=47的數據加鎖;此時不管是sessionA想對id=47的行加鎖,或是sessionB想對id=1的數據行加鎖,

均會阻塞,併產生死鎖。

image-20231113181648750

死鎖的關鍵在於:兩個(或以上)的Session加鎖的順序不一致,mysql是逐行加鎖的

解決方案:根據索引回查主鍵id並排序,根據主鍵id更新。

查詢最近一次死鎖日誌:SHOW ENGINE INNODB STATUS;

死鎖日誌如下:

0: len 8; hex 8000000000000001; asc ;;------事務更新的主鍵索引-----第一個字節是 "80",轉換爲十進制爲 128。後面的 7 個字節 "000000000000002f" 代表一個 64 位的整數,轉爲十進制爲 1
1: len 6; hex 000000000879; asc y;;-----事務id2169
2: len 7; hex 020000016b0151; asc k Q;;---事務回滾指針
3: len 8; hex 7a68616e6773616e; asc zhangsan;;----代表數據值-----十六進制轉化後爲:zhangsan
4: len 4; hex 8000000f; asc ;;------代表數據值------六進制轉化後爲:15
5: len 7; hex 8000000000d200; asc ;;------代表數據值salary

  • 記錄鎖(LOCK_REC_NOT_GAP): lock_mode X locks rec but not gap
  • 間隙鎖(LOCK_GAP): lock_mode X locks gap before rec
  • Next-key鎖(LOCK_ORNIDARY): lock_mode X / lock mode S
  • 插入意向鎖(LOCK_INSERT_INTENTION): lock_mode X locks gap before rec insert intention

=====================================
2023-11-15 13:45:36 0x70bc INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 1 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 247 srv_active, 0 srv_shutdown, 95073 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 928
OS WAIT ARRAY INFO: signal count 816
RW-shared spins 0, rounds 0, OS waits 0
RW-excl spins 0, rounds 0, OS waits 0
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-11-15 13:40:30 0x14de8
*** (1) TRANSACTION:
TRANSACTION 2171, ACTIVE 0 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1128, 4 row lock(s), undo log entries 1
MySQL thread id 61, OS thread handle 77680, query id 6470 localhost 127.0.0.1 root updating
UPDATE test_user_info  SET salary = salary + 1  
 
 WHERE (age > 12 AND age < 25)

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 2 page no 4 n bits 80 index PRIMARY of table `my_test`.`test_user_info` trx id 2171 lock_mode X locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 8; hex 800000000000002f; asc        /;;-----事務更新的主鍵索引-----第一個字節是 "80",轉換爲十進制爲 128。後面的 7 個字節 "000000000000002f" 代表一個 64 位的整數,轉爲十進制爲 47
 1: len 6; hex 00000000087b; asc      {;;
 2: len 7; hex 01000001720151; asc     r Q;;
 3: len 8; hex 7a68616e6773616e; asc zhangsan;;
 4: len 4; hex 8000000d; asc     ;;
 5: len 7; hex 8000000000d200; asc        ;;


*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2 page no 4 n bits 80 index PRIMARY of table `my_test`.`test_user_info` trx id 2171 lock_mode X locks rec but not gap waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 8; hex 8000000000000001; asc         ;;-----事務更新的主鍵索引------第一個字節是 "80",轉換爲十進制爲 128。後面的 7 個字節 "000000000000002f" 代表一個 64 位的整數,轉爲十進制爲 1
 1: len 6; hex 000000000879; asc      y;;
 2: len 7; hex 020000016b0151; asc     k Q;;
 3: len 8; hex 7a68616e6773616e; asc zhangsan;;
 4: len 4; hex 8000000f; asc     ;;
 5: len 7; hex 8000000000d200; asc        ;;


*** (2) TRANSACTION:
TRANSACTION 2169, ACTIVE 0 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1128, 4 row lock(s), undo log entries 1
MySQL thread id 60, OS thread handle 12696, query id 6468 localhost 127.0.0.1 root updating
UPDATE test_user_info  SET salary = salary + 1  
 
 WHERE (name = 'zhangsan')

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 2 page no 4 n bits 80 index PRIMARY of table `my_test`.`test_user_info` trx id 2169 lock_mode X locks rec but not gap
Record lock, heap no 7 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 8; hex 8000000000000001; asc         ;;------事務更新的主鍵索引-----第一個字節是 "80",轉換爲十進制爲 128。後面的 7 個字節 "000000000000002f" 代表一個 64 位的整數,轉爲十進制爲 1
 1: len 6; hex 000000000879; asc      y;;-----事務id2169
 2: len 7; hex 020000016b0151; asc     k Q;;---事務回滾指針
 3: len 8; hex 7a68616e6773616e; asc zhangsan;;----代表數據值-----十六進制轉化後爲:zhangsan
 4: len 4; hex 8000000f; asc     ;;------代表數據值------六進制轉化後爲:15
 5: len 7; hex 8000000000d200; asc        ;;------代表數據值salary


*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2 page no 4 n bits 80 index PRIMARY of table `my_test`.`test_user_info` trx id 2169 lock_mode X locks rec but not gap waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 8; hex 800000000000002f; asc        /;;-------事務更新的主鍵索引----第一個字節是 "80",轉換爲十進制爲 128。後面的 7 個字節 "000000000000002f" 代表一個 64 位的整數,轉爲十進制爲 47
 1: len 6; hex 00000000087b; asc      {;;
 2: len 7; hex 01000001720151; asc     r Q;;
 3: len 8; hex 7a68616e6773616e; asc zhangsan;;
 4: len 4; hex 8000000d; asc     ;;
 5: len 7; hex 8000000000d200; asc        ;;

*** WE ROLL BACK TRANSACTION (2)
------------
TRANSACTIONS
------------
Trx id counter 2362
Purge done for trx's n:o < 2362 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 283442874948104, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 283442874947328, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 283442874946552, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 283442874945776, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
--------
FILE I/O
--------
I/O thread 0 state: wait Windows aio ((null))
I/O thread 1 state: wait Windows aio (insert buffer thread)
I/O thread 2 state: wait Windows aio (read thread)
I/O thread 3 state: wait Windows aio (read thread)
I/O thread 4 state: wait Windows aio (read thread)
I/O thread 5 state: wait Windows aio (read thread)
I/O thread 6 state: wait Windows aio (write thread)
I/O thread 7 state: wait Windows aio (write thread)
I/O thread 8 state: wait Windows aio (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
 ibuf aio reads:
Pending flushes (fsync) log: 0; buffer pool: 0
955 OS file reads, 5540 OS file writes, 3533 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
 insert 0, delete mark 0, delete 0
discarded operations:
 insert 0, delete mark 0, delete 0
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 2 buffer(s)
Hash table size 34679, node heap has 2 buffer(s)
Hash table size 34679, node heap has 3 buffer(s)
Hash table size 34679, node heap has 2 buffer(s)
Hash table size 34679, node heap has 3 buffer(s)
Hash table size 34679, node heap has 2 buffer(s)
Hash table size 34679, node heap has 2 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s
---
LOG
---
Log sequence number          20808942
Log buffer assigned up to    20808942
Log buffer completed up to   20808942
Log written up to            20808942
Log flushed up to            20808942
Added dirty pages up to      20808942
Pages flushed up to          20808942
Last checkpoint at           20808942
Log minimum file id is       6
Log maximum file id is       6
1753 log i/o's done, 0.00 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 0
Dictionary memory allocated 622315
Buffer pool size   8191
Free buffers       6881
Database pages     1293
Old database pages 457
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 816, not young 4439
0.00 youngs/s, 0.00 non-youngs/s
Pages read 933, created 367, written 2566
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 1293, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Process ID=81884, Main thread ID=79028 , state=sleeping
Number of rows inserted 50, updated 535, deleted 22, read 1335
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
Number of system rows inserted 470, updated 362, deleted 168, read 21717
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================

5.3元數據'鎖表'場景(DDL)

MySQL 內部對於 DDL 的 ALGORITHM 有兩種選擇:INPLACE 和 COPY(8.0 新增了 INSTANT,但是使用範圍較小)。

在mysql5.6.71之前,都是使用COPY的方式。

COPY的原理

1、鎖表,期間DML不可並行執行
2、生成臨時表以及臨時表文件(.frm.ibd)
3、拷貝原表數據到臨時表
4、重命名臨時表以及文件
5、刪除原表以及文件
6、提交事務、釋放鎖

但是在之後的版本,官方開始支持INPLACE的 ALTER TABLE 操作來避免數據拷貝,同時支持了在線上 DDL 的過程中不阻塞 DML 操作,真正意義上的實現了 Online DDL。然而並不是所有的 DDL 操作都支持在線操作。

語句如下:

ALTER TABLE table_name ADD INDEX index_name (column_name), ALGORITHM=INPLACE, LOCK=NONE;

我們常說的 Online DDL,其實是從 DML 操作的角度描述的,如果 DDL 操作不阻塞 DML 操作,那麼這個 DDL 就是 Online 的。當前非 Online 的 DDL 其實已經比較少了,主要有:

  • 新增全文索引
  • 新增空間索引
  • 刪除主鍵
  • 修改列數據類型
  • 指定表字符集
  • 修改表字符集

在執行 DDL 操作時,MySQL 內部對於 ALGORITHM 的選擇策略是:如果用戶顯式指定了 ALGORITHM,那麼使用用戶指定的選項;如果用戶未指定,那麼如果該操作支持 INPLACE 則優先選擇 INPLACE,否則選擇 COPY;

  1. COPY 算法執行的 DDL 肯定不是 Online 的;
  2. INPLACE 算法執行的 DDL 不一定是 Online 的;

INPLACE 原理

  1. prepare階段:嘗試獲取MDL排他鎖,禁止其他線程讀寫;
  2. ddl執行階段:降級成MDL共享鎖,允許其他線程讀取;
  3. commit階段:升級成MDL排他鎖,禁止其他線程讀寫;
  4. finish階段:釋放MDL鎖;

下面演示兩種常見的操作,添加索引和添加字段

5.3.1添加索引

下面我們演示添加索引(支持online-ddl)的過程:

SessionA執行:

select * from test_user_info where name = 'lishi'

不提交;

SessionB執行:

alter table test_user_info
add index name_age_index (name, age)

不提交;

SessionC執行:

select * from test_user_info where name = 'lishi'

不提交;

image-20231115163440850

我們發現,SessionC的查詢也阻塞了。

這時,我們提交SessionA, 我們發現SessionC可以自動執行了,但是SessionB的添加索引還是阻塞。

image-20231115163650795

現象1: SessionA的查詢未提交,阻塞SessionB的執行;SessionB被阻塞後,阻塞SessionC的查詢;

分析: SessionA的查詢未提交,表加元數據讀鎖;SessionB執行,獲取到表的元數據寫鎖,因爲元數據讀寫鎖互斥,SessionB被阻塞;SessionC執行,獲取表的元數據讀鎖,因爲元數據讀寫鎖互斥,SessionC被阻塞。

問題: 原本可以並行執行的操作被阻塞了。

現象2: SessionA提交事務,此時SessionC優先執行,SessionB繼續被阻塞;SessionC提交事務後,SessionB纔可以執行。

結論:當DDL添加索引因爲其他DML操作阻塞時,需要等待其他所有DML操作都提交後才能執行,即使在阻塞後有其他DML操作進來,其他DML操作也優先於DDL添加索引執行。

這會造成什麼問題?

假如一個表的併發訪問很高,DDL操作可能會超時。如果數據庫有慢查詢,比如如下查詢,查詢返回數據需要30秒:

SELECT SLEEP(30)  FROM my_test.test_user_info WHERE NAME = 'lisi'

因爲DDL執行時,會阻塞其之後執行的DML操作,可能導致之後的DML操作積壓,也會造成單個DML操作返回時間過長。可能造成業務系統數據庫連接被打滿,接口超時,即使不超時也會因爲返回時間過長影響用戶體驗。這種現象看來,就彷彿表被鎖住了一樣,雖然不是真正意義上的鎖表。

所以我們在執行ALTER操作的時候,儘量在業務不繁忙的階段執行。

5.3.2添加字段

SessionA執行:

SELECT *  FROM my_test.test_user_info WHERE NAME = 'lisi'

SessionB執行:

alter table test_user_info
add column  hometown varchar(255) default null

SessionC執行:

update test_user_info set salary = salary+1 where age = 12

SessionD執行:

delete from test_user_info where id = 100

均不自動提交事務

image-20231115171535140

現象1: SessionA的查詢未提交,阻塞SessionB的執行;SessionB被阻塞後,阻塞SessionC的執行;SessionB被阻塞後,阻塞SessionD的執行;

分析: SessionA的查詢未提交,表加元數據讀鎖;SessionB執行,獲取到表的元數據寫鎖,因爲元數據讀寫鎖互斥,SessionB被阻塞;SessionC執行,獲取表的元數據讀鎖,因爲元數據讀寫鎖互斥,SessionC被阻塞;SessionD執行,獲取表的元數據讀鎖,因爲元數據讀寫鎖互斥,SessionD被阻塞。

現象2: SessionA提交事務,此時SessionB,SessionC,SessionD全部自動執行。

結論:當DDL添加字段因爲其他DML操作阻塞時,需要等待其他所有DML操作都提交後才能執行,當阻塞DDL添加字段的DML執行後,DDL添加字段和後續的DML均可同步執行。這點和添加索引完全不同。

5.4.pt-online-schema-change

pt-online-schema-change 是一個用於在線更改 MySQL 表結構的工具,它是 Percona Toolkit 的一部分。它的原理是通過在線複製表數據,同時在新表上應用修改,從而避免了直接修改原始表結構導致的鎖表和性能下降問題。

pt-online-schema-change 的原理:

  1. 創建一張新表,表結構與舊錶相同;
  2. Alter 新表;
  3. 在原表上創建 insert、update、delete 三種類型的觸發器;
  4. 將舊錶的數據拷貝到新表中,同時通過觸發器將舊錶中的操作映射到新表;
  5. 如果原表有外鍵約束,處理外鍵;
  6. 原表重命名爲 old 表,new 表重命名爲原表,整個過程爲原子操作;
  7. 刪除 old 表(默認);

pt-online-schema-change 在以下場景中特別有用:

  1. 修改大型表的結構:對於包含數百萬甚至數十億行的大型表,直接修改表結構可能導致長時間的鎖表和性能下降。pt-online-schema-change 通過在線方式避免了這些問題。
  2. 避免業務中斷:在需要修改生產環境數據庫表結構時,pt-online-schema-change 可以在不影響業務正常運行的情況下進行表結構更改。
  3. 兼容各種存儲引擎:pt-online-schema-change 支持各種 MySQL 存儲引擎,如 InnoDB 和 MyISAM。

7.建議

mysql在非唯一索引上的操作,會加間隙鎖或臨鍵鎖;根據數據的分散情況,可能產生大量的鎖,可能導致大量的行或間隙被鎖定。阻塞其他事務的insert,update,delete等操作,有產生死鎖的風險,對數據庫性能影響較大。對mysql的insert,update,delete或select for update等操作,建議都在唯一索引上進行。實際業務中,我們要根據二級索引更新數據前,可以先將數據的主鍵查詢出來,再根據主鍵批量操作,這樣雖然增加了查詢操作,但是隻會在唯一索引上加記錄鎖,鎖定行數較少,對其他事務的操作影響較小,提高了數據庫的寫的性能。

對於表的DDL操作,建議在業務不繁忙的時間段執行,或依賴第三方工具,如pt-online-schema-change來處理。

8.參考文檔

MySql8.0官方文檔:

https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-next-key-locks

MySQL8.0優化 - 鎖 - 從數據操作的粒度劃分:

https://blog.csdn.net/ChinaYangJu/article/details/127939056

詳細剖析MySQL臨鍵鎖:

https://blog.csdn.net/Bb15070047748/article/details/131766686

mysql隔離級別RR下的行鎖、臨鍵鎖、間隙鎖詳解及運用:

https://blog.csdn.net/qq_35572020/article/details/127629464

pt-online-schema-change

https://blog.csdn.net/weixin_44352521/article/details/108558026

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