MySQL的鎖
1、MySQL的鎖的基本介紹
鎖是計算機協調多個進程或線程并發訪問某一資源的機制。在數據庫中,除傳統的計算資源(如CPU、RAM、I/O等)的爭用以外,數據也是一種供許多用戶共享的資源。如何保證數據并發訪問的一致性、有效性是所有數據庫必須解決的一個問題,鎖沖突也是影響數據庫并發訪問性能的一個重要因素。從這個角度來說,鎖對數據庫而言顯得尤其重要,也更加復雜。
相對其他數據庫而言,MySQL的鎖機制比較簡單,其最顯著的特點是不同的存儲引擎支持不同的鎖機制。
(InnoDB 存儲引擎(MySQL 默認存儲引擎,默認可重復讀隔離級別)中)數據庫事務的鎖是執行某語句時就會加鎖,而且只有當整個事務完成(提交或回滾)后,鎖(排他行鎖、共享行鎖等等)才會被釋放。例如,有一個事務 T1,在其中執行了UPDATE users SET user_email = 'new_email@example.com' WHERE user_id = 1001。當這條UPDATE語句執行后,排他行鎖就會鎖住user_id = 1001對應的行。即使UPDATE語句執行完成,只要事務 T1 還沒有提交或者回滾,這個排他行鎖就會一直存在。
1.1、數據庫鎖的分類
MySQL 的鎖按照范圍可以分為全局鎖(對整個數據庫實例進行加鎖)、表鎖、行鎖,其中行鎖是由數據庫引擎實現的,并不是所有的引擎都提供行鎖,MyISAM 就不支持行鎖。
從對數據操作的類型:
- 共享鎖(讀鎖):針對同一行數據,多個讀操作可以同時進行而不會互相影響,但會阻塞寫操作。
- 排它鎖(寫鎖):當前寫操作沒有完成前,它會阻斷其他會話的寫和讀。
共享鎖會阻塞涉及到的數據的寫,但是不會堵塞讀;而排它鎖則會把涉及到的數據的讀和寫都堵塞。也就是共享鎖和排它鎖都會阻塞涉及到的數據的寫操作。
從對數據操作的粒度分:
- 表級鎖:開銷小,加鎖快;鎖定粒度大,發生鎖沖突的概率最高,并發度最低。
- 行級鎖:開銷大,加鎖慢;鎖定粒度最小,發生鎖沖突的概率最低,并發度也最高。
- 頁面鎖:開銷和加鎖時間界于表鎖和行鎖之間;鎖定粒度界于表鎖和行鎖之間,并發度一般
2、表級鎖(共享鎖、排它鎖)
2.1、表級鎖的分類
2.1.1、共享表鎖(讀鎖,其他會話對該表無法增刪改)
假設有會話 A 和 會話 B,會話 A 對 mylock 表加了讀鎖,則:
- 會話A :可以查該表的數據,但是無法增刪改該表的數據,并且也無法查詢其他表的數據,即使其他表未被鎖定也無法查詢,直到對該鎖表進行釋放鎖。
- 會話B :可以查詢該鎖表的數據,也可以查詢、增刪改其他表的數據。但是在對已鎖的數據進行增刪改時,會一直阻塞,導致SQL無法執行結束,直到該表鎖被釋放后,SQL會自動執行結束。
| 已鎖定表 | 其他表 | ||
| 會話A(加鎖的會話) | 可查;但無法增刪改 | 無法增刪改查(會報錯) | |
| 其他會話 | 可查;但無法增刪改(會一直阻塞) | 可增刪改查 | |
也就是加共享鎖的會話可以查鎖表數據,但加鎖會話無法增刪改該表,也無法查詢其他表。其他的會話可以查詢任何表的數據,但其他會話對鎖表進行增刪改時也會阻塞,直到鎖表的鎖被釋放。
總結:共享鎖會阻塞寫,不會阻塞讀。
2.1.2、排它表鎖(寫鎖,其他會話對該表無法增刪改查)
假設有會話 A 和 會話 B,會話 A 對 mylock 表加了寫鎖,則:
- 會話A :可以查該表的數據,也可以對該鎖定表進行增刪改,但是無法查詢其他表的數據,即使其他表未被鎖定也無法查詢,直到對已鎖定表進行釋放鎖。
- 會話B :對已鎖定表進行增刪改查都會一直阻塞,直到該表鎖被釋放??梢詫ζ渌磉M行增刪改查。
| 已鎖定表 | 其他表 | |
| 會話A(加鎖的會話) | 可增刪改查 | 無法增刪改查(會報錯) |
| 其他會話 | 無法增刪改查(會一直阻塞) | 可增刪改查 |
也就是加排它鎖的會話可以增刪改查鎖定表的數據,但加鎖會話無法查詢其他表。其他的會話對鎖定表進行增刪改查時會一直阻塞,直到鎖表的鎖被釋放,但可以增刪改查其他表的數據。
總結:寫鎖會阻塞其他會話的讀和寫。
2.2、表級鎖之MyISAM 引擎表的讀寫操作和鎖
MyISAM 存儲引擎在執行查詢語句(select)時,會自動給涉及的所有表都加上共享鎖,在執行增刪改操作前,會自動給涉及到的所有表都加上排它鎖。
所以對 MyISAM 表進行操作,會有以下情況:
- 對 MyISAM 表進行讀操作時(會自動加共享鎖):不會阻塞其他進程對同一表的讀請求,但會阻塞對同一表的寫請求。只有當讀鎖釋放后,才會執行其它進程的寫操作。
- 對 MyISAM 表進行寫操作時〈會自動加排它鎖):會阻塞其他進程對同一表的讀和寫操作,只有當寫鎖釋放后,才會執行其它進程對該表的讀寫操作。
MyISAM 的讀寫鎖調度是寫優先,這也是 MyISAM 不適合做寫為主的表的引擎的原因。因為寫操作會自動加寫鎖,加鎖后其他線程就沒辦法做任何操作,大量的寫操作會使查詢很難得到鎖,從而造成永遠阻塞。
2.3、表級鎖的相關SQL語句
2.3.1、查看被鎖定的表
可以通過以下命令來查看哪些表被鎖定:
show open tables;
結果示例如下:

結果說明如下:
- Database:含有該表的數據庫
- Table:表名稱
- In_use:表當前被查詢使用的次數。如果該數為零,則表是打開的,即當前沒有鎖表。如果該列的值大于 0,則表示表正在被使用,有可能被鎖住。
- Name_locked:表名稱是否被鎖定。名稱鎖定用于取消表或對表進行重命名等操作
在 MySQL 的 InnoDB 引擎中,information_schema.INNODB_LOCKS表存儲了當前 InnoDB 存儲引擎中存在的鎖信息。可以通過查詢這個表來確定表是否被鎖以及被何種鎖鎖住。
SELECT * FROM information_schema.INNODB_LOCKS
通過查詢結果可以看到哪些表被鎖住,并且可以看到鎖的類型(如行鎖、表鎖)、鎖定的事務 ID 等信息。
2.3.2、給表加表級鎖
可以使用以下命令給表加鎖:
lock table 表名 read(write), 表名2 read(write), 其他;
-- 示例
lock table lock_table_test read, lock_table_test2 write;
示例:
先創建一個表 mylock:
create table mylock (
id int not null primary key auto_increment,
name varchar(20) default ''
) engine myisam;
insert into mylock(name) values('a');
insert into mylock(name) values('b');
insert into mylock(name) values('c');
insert into mylock(name) values('d');
insert into mylock(name) values('e');
然后給該表加讀鎖:
lock table mylock read;
使用 show open tables; 來查看表鎖情況:

可以看到,mydbtest.mylock 表已被鎖定。
2.3.3、釋放表鎖
釋放所有表的鎖:
unlock tables;
2.3.4、分析表鎖
我們可以通過以下命令行來分析表鎖的情況:
show status like 'table_locks%';
結果示例:

可以通過檢查 table_locks_waited(主要分析該值) 和 table_locks_immediate 狀態變量來分析系統上的表鎖定,說明如下:
- table_locks_waited:出現表級鎖定爭用而發生等待的次數(也可以說是不能立即獲取鎖的次數,每觸發一次需等待鎖的SQL時該值就加1),此值高則說明存在著較嚴重的表級鎖爭用情況。
- table_locks_immediate:產生表級鎖定的次數,表示可以立即獲取鎖的查詢次數,每立即獲取鎖值加1。
3、行級鎖
MyISAM 引擎支持表級鎖,但不支持行級鎖,InnoDB 引擎支持行級鎖和表級鎖。在默認的可重復讀隔離級別下,InnoDB 會盡量使用行級鎖(而不是表級鎖)和 MVCC(多版本并發控制)機制來保證事務的隔離性。
在 MySQL 的 InnoDB 存儲引擎中,行級鎖的使用通常與索引密切相關。當通過索引條件來查詢和修改數據時,InnoDB 更傾向于使用行級鎖。這是因為索引能夠幫助數據庫快速定位到要操作的行,從而精確地對這些行加鎖。例如,在一個employees表中有employee_id索引,當執行UPDATE employees SET salary = salary * 1.1 WHERE employee_id = 1001這樣的更新操作時,InnoDB 可以通過employee_id索引快速找到employee_id = 1001對應的行,并對該行加排他行鎖(X 鎖),其他事務對這一行的讀寫操作(不符合 MVCC 機制下的可讀條件)就會被阻塞。
當查詢或修改數據的條件沒有使用索引時,InnoDB 也有可能會升級為表鎖。這是因為如果沒有索引來定位行,為了保證操作的一致性和正確性,數據庫可能會選擇更粗粒度的鎖,即表鎖。例如,在employees表中,如果執行UPDATE employees SET department = 'New Dept' WHERE age > 30,假設age列沒有索引,InnoDB 可能會對整個employees表加表鎖。這樣做的原因是,在沒有索引的情況下,數據庫難以確定哪些行滿足條件,通過加表鎖可以防止其他事務對表進行不兼容的操作,確保當前事務能夠正確地完成更新操作。不過,這種行為也可能因 MySQL 的版本、配置參數以及具體的操作場景而有所不同。
示例:
下面建一張表,并且建立索引,因為只有通過索引來檢索數據,InnoDB 才會使用行級表。
CREATE TABLE test_lock (a INT(11),b VARCHAR(16))ENGINE=INNODB; INSERT INTO test_lock VALUES(1,'b2'); INSERT INTO test_lock VALUES(3,'3'); INSERT INTO test_lock VALUES(4, '4000'); INSERT INTO test_lock VALUES(5,'5000'); INSERT INTO test_lock VALUES(6, '6000'); INSERT INTO test_lock VALUES(7,'7000'); INSERT INTO test_lock VALUES(8, '8000'); INSERT INTO test_lock VALUES(9,'9000'); INSERT INTO test_lock VALUES(1,'b1'); -- 建立索引列 CREATE INDEX test_lock_a_ind ON test_lock(a); CREATE INDEX test_lock_b_ind ON test_lock(b);
建立兩個會話,并且設置不自動提交事務(這樣才能模擬出在一個事務內的情況,否則事務立馬提交就看不出效果了),先在 A 會話中對某一行數據進行修改但不提交,然后在 B 會話中也對同一行數據進行修改,如下:

此時,會話 B 的修改會一直阻塞,直到 A 提交事務 commit,會話 B 的進程才能執行成功,否則會一直阻塞直到超時報錯,這就是加了行級鎖。
當然,如果會話 B 修改其他行的數據則是不會受到影響的,因為行級鎖只會鎖定指定行的數據。而且如果只是查詢則沒問題,比如A只是查詢,則不會鎖行,或者A修改B查詢,則B的查詢不會受到影響。
3.1、行級鎖的分類
定義與原理:共享行鎖用于對一行數據進行讀取操作時的并發控制。當一個事務對某一行數據加上共享行鎖(其實默認情況下普通查詢并不會加鎖)后,允許其他事務也對這一行加共享行鎖來讀取數據。但是,如果其他事務想要對這行數據進行修改,則需要獲取排他行鎖,此時事務會被阻塞,直到所有的共享行鎖都被釋放。這是基于并發讀取數據的需求,多個事務可以同時讀取同一行數據而不會相互干擾,只要它們都只進行讀取操作。
應用場景和示例:以一個在線圖書閱讀平臺為例,多個用戶可能同時查看某一本書的詳細信息(如書名、作者、簡介等)。假設這些信息存儲在books表中的一行數據中,當用戶 A 的事務對這行數據加共享行鎖進行讀取時,用戶 B 的事務也可以對同一行數據加共享行鎖來讀取相同的信息。這樣可以滿足多個用戶同時獲取圖書信息的需求,并且在沒有寫操作介入時,不會產生沖突。但是,如果有一個事務想要對這行數據進行修改(如更新圖書簡介),則需要獲取排他行鎖,此時該事務會被阻塞,直到所有的共享行鎖都被釋放。
- 排他行鎖(Exclusive Row Lock,X)
定義與原理:排他行鎖用于對一行數據進行寫操作(如插入、更新、刪除)時的并發控制。當一個事務對某一行數據加上排他行鎖后,只有該事務能夠對這一行進行讀寫操作,其他事務不能對這一行加共享行鎖進行讀取,也不能加排他行鎖進行寫入,直到持有排他行鎖的事務完成并釋放鎖。這種鎖機制確保了在對數據進行修改時的獨占性,防止其他事務對正在修改的數據進行干擾,從而保證數據的準確性和完整性。
應用場景和示例:在一個銀行賬戶管理系統中,當一個事務需要更新某個賬戶的余額時,它會對存儲該賬戶余額的行數據加上排他行鎖。例如,事務 A 要從賬戶 1 向賬戶 2 轉賬,事務 A 會對賬戶 1 和賬戶 2 余額所在的行分別加上排他行鎖。在事務 A 完成轉賬操作(包括更新兩個賬戶的余額并提交事務)之前,其他事務(如同時進行的另一個轉賬事務或者查詢賬戶余額的事務)對這兩個賬戶余額行的讀寫操作都會被阻塞。這樣可以確保轉賬操作的原子性,即要么全部完成,要么全部不做,避免數據不一致的情況出現。- 意向共享行鎖(Intention Shared Row Lock,IS)
定義與原理:意向共享行鎖是一種表級的鎖意向表示,該鎖是為了提高效率用的。當一個事務對表中的一行或多行加上共享行鎖時,會在表級自動加上意向共享行鎖。它的主要作用是為了提高數據庫在處理更高層次(表級)鎖沖突檢查時的效率。通過在表級設置意向共享行鎖,數據庫在進行表級鎖操作(如另一個事務試圖對整個表加排他表鎖)時,可以快速判斷表內是否有行已經被加上共享行鎖,而不需要逐行檢查。
應用場景和示例:假設在一個電商系統的庫存管理模塊中,有一個事務需要查詢多個商品的庫存信息行。事務在對這些商品庫存信息行加上共享行鎖的同時,會在inventory表級加上意向共享行鎖。這樣,當其他事務想要對整個inventory表進行排他操作(如更新所有庫存信息)時,數據庫通過檢查表級的意向共享行鎖,就可以快速判斷出表內有行正在被讀取,從而避免了可能的鎖沖突,提高了系統的并發性能和鎖管理效率。
-
意向排他行鎖(Intention Exclusive Row Lock,IX)
定義與原理:意向排他行鎖同樣是一種表級的鎖意向表示,該鎖是為了提高效率用的。當一個事務對表中的一行或多行加上排他行鎖時,會在表級自動加上意向排他行鎖。它的主要作用是在更高層次上表明表內有行正在進行排他操作,方便數據庫快速進行鎖沖突檢查。當其他事務嘗試對整個表進行不兼容操作(如加共享鎖或排他鎖)時,數據庫可以通過檢查表級的意向排他行鎖及時發現并阻止,從而避免復雜的逐行檢查過程。
應用場景和示例:在一個訂單處理系統中,當事務 A 對訂單表中的某一訂單行進行更新操作(如修改訂單狀態)時,會對該訂單行加上排他行鎖,并在訂單表級加上意向排他行鎖。此時,如果另一個事務 B 想要對整個訂單表進行讀取操作(加共享鎖)或者進行其他修改操作(加排他鎖),數據庫通過檢查表級的意向排他行鎖,就可以快速知道表內有行正在進行排他操作,從而避免了可能的鎖沖突,確保數據的一致性和操作的正確性。
3.2、索引失效行鎖變表鎖
InnoDB 行鎖是通過給索引上的索引項加鎖來實現的,只有通過索引條件來查詢修改數據(select xxx where 索引字段),InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!行級鎖都是基于索引的,如果一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖。
比如,上面的例子當中,如果是修改 a 的值,并且條件不加引號,如條件語句為 where b = 1000,由于 b 是 varchar 類型,而此時不使用引號引起來會導致 mysql 底層自動進行類型轉換,相當于發生了計算,由此索引會失效。當沒有使用到索引時,此時 InnoDB 表不會使用行級鎖,而是使用表鎖。
如下:

上面 A 會話中由于 SQL 語句中的條件發生了類型轉換,導致索引失效,行鎖變表鎖,所以 B 會話的修改會一直阻塞,直到 A 會話提交事務 B 的修改才能執行結束。
3.3、間隙鎖(Next-Key鎖)
當我們用范圍條件而不是相等條件來修改數據,InnoDB 會給符合范圍條件的所有數據記錄的索引項加鎖,那些值在條件范圍內但并不存在在表里的數據,叫做“間隙(GAP)”,InnoDB 也會對這些 “間隙” 加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。可以理解是一個范圍鎖。
比如,A會話對某個范圍條件的數據進行修改,但 A 還未提交事務,此時 B 會話想新增一條數據,但剛好這條數據符合 A 修改語句的條件范圍,則 B 的新增會一直阻塞,因此此時 InnoDB 對這些范圍數據加了間隙鎖。
如下:

3.4、如何分析行鎖
可通過以下命令查看數據庫中行鎖的情況:
show status like 'innodb_row_lock%';

說明如下:
- Innodb_row_lock_current_waits:當前正在等待鎖定的數量
- Innodb_row_lock_time:從系統啟動到現在鎖定總時間長度。
- Innodb_row_lock_time_avg:每次等待所花平均時長。
- Innodb_row_lock_time_max:從系統啟動到現在等待最長的一次所花的時間
- Innodb_row_lock_waits:系統啟動后到現在總共等待的次數
當等待的次數很高,而且每次等待的時長也不小的時候,我們就需要分析系統中為什么會有如此多的等待,然后根據分析結果著手制定優化計劃。
4、MySQL 默認事務的鎖
mysql 默認事務隔離(Repeatable Read)下,增刪改查操作對應的鎖機制:
4.1、SELECT ... FROM
1)SELECT ... FROM ...
- 該語句是一個快照讀,通過讀取數據庫的一個快照,不會加任何鎖,除非將隔離級別設置成了 SERIALIZABLE 。
2)SELECT ... FROM ... LOCK IN SHARE MODE
- 如果使用到索引并且條件是范圍,則在所有索引掃描范圍的索引記錄上加上共享的間隙鎖(next key鎖)。如果是唯一索引,只需要在相應記錄上加排他行鎖。這些被共享鎖鎖住的行無法進行update/delete。
- 允許其它事務對這些記錄再加SHARE鎖
3)SELECT ... FROM ... FOR UPDATE
- 如果使用到索引并且條件是范圍,則在所有索引掃描范圍的索引記錄上加上排他的next key鎖。如果是唯一索引,只需要在相應記錄上加排他行鎖
4.2、UPDATE ... WHERE ...
- 對要更新的行加上排他行鎖。
- 如果涉及范圍更新,則會對索引間隙加上間隙鎖。
4.3、DELETE FROM ... WHERE ...
- 語句在所有索引掃描范圍的索引記錄上加上排他的next key鎖。如果是唯一索引,只需要在相應記錄上加排他行鎖
4.4、INSERT
- 在插入的記錄上加一把排他行鎖。
總結:
隔離級別越高,越能保證數據的完整性和一致性,但是對并發性能的影響也越大,魚和熊掌不可兼得啊。對于多數應用程序,可以優先考慮把數據庫系統的隔離級別設為Read Committed,它能夠避免臟讀取,而且具有較好的并發性能。盡管它會導致不可重復讀、幻讀這些并發問題,在可能出現這類問題的個別場合,可以由應用程序采用悲觀鎖或樂觀鎖來控制。
可參考:http://www.rzrgm.cn/frankyou/p/9603784.html
5、數據庫的事務隔離和鎖的關系
事務隔離級別是為了解決多并發時查到的數據不一致的問題,而鎖是為了阻塞其他事務對同一數據進行修改。一個是為查,一個是為改。
6、多并發事務產生死鎖
6.1、分析和示例
在 MySQL 默認的隔離級別(Repeatable Read)中,事務在更新多行數據時(比如上述依次更新account_id = 1和account_id = 2 兩行數據)會依次對它們加上排他行鎖,并且這些鎖會在事務結束時統一釋放。即在可重復讀隔離級別下,多行數據的排他行鎖會在事務提交(COMMIT)或者回滾(ROLLBACK)時才會統一釋放,以確保事務的一致性和可重復讀特性。
據 “鎖的重入性”(同一事務對已持有的鎖可重復獲取),所以在同一個事務中,先后兩次更新同一行數據并不會產生死鎖,死鎖的核心是 “兩個或多個事務相互等待對方釋放鎖”。
6.2、如何預防死鎖

浙公網安備 33010602011771號