大家好,我是melo,一名大三后臺練習生,最近趕在春招前整理整理發過的博客~??????!
??引言
鎖鎖鎖,到哪到離不開這樁瑣事,并發瑣事,redis瑣事,如今是MySQL瑣事,這其中瑣事,還跟MySQL另一個重要的東西--事務息息相關,
這篇將從以下幾點,帶你解開這把愛情的苦鎖:
??本篇速覽腦圖
??常規表鎖&行鎖
這一部分較為常規,若有前置知識,可以直接跳到下邊的【表級鎖擴展】部分開始閱讀
建議借助側邊欄,有emoji表情的屬于重點
鎖概述
鎖是計算機協調多個行程或執行緒并發訪問某一資源的機制(避免爭搶),
在資料庫中,除傳統的計算資源(如 CPU、RAM、I/O 等)的爭用以外,資料也是一種供許多用戶共享的資源,如何保證資料并發訪問的一致性、有效性是所有資料庫必須解決的一個問題,鎖沖突也是影響資料庫并發訪問性能的一個重要因素,從這個角度來說,鎖對資料庫而言顯得尤其重要,也更加復雜,
鎖分類
從對資料操作的粒度分 :
1) 表鎖:操作時,會鎖定整個表,
2) 行鎖:操作時,會鎖定當前操作行,
從對資料操作的型別分:
1) 讀鎖(共享鎖):針對同一份資料,多個讀操作可以同時進行而不會互相影響,
2) 寫鎖(排它鎖):當前操作沒有完成之前,它會阻斷其他寫鎖和讀鎖,
Mysql 鎖
相對其他資料庫而言,MySQL的鎖機制比較簡單,其最顯著的特點是不同的存盤引擎支持不同的鎖機制,下表中羅列出了各存盤引擎對鎖的支持情況:
| 存盤引擎 | 表級鎖 | 行級鎖 | 頁面鎖(了解) |
|---|---|---|---|
| MyISAM | 支持 | 不支持 | 不支持 |
| InnoDB | 支持 | 支持(默認) | 不支持 |
| MEMORY | 支持 | 不支持 | 不支持 |
| BDB | 支持 | 不支持 | 支持 |
MySQL這3種鎖的特性可大致歸納如下 :
| 鎖型別 | 特點 |
|---|---|
| 表級鎖 | 偏向MyISAM 存盤引擎,開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的概率最高,并發度最低, |
| 行級鎖 | 偏向InnoDB 存盤引擎,開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,并發度也最高, |
| 頁面鎖 | 開銷和加鎖時間界于表鎖和行鎖之間;會出現死鎖;鎖定粒度界于表鎖和行鎖之間,并發度一般, |
粒度小,自然發生鎖沖突的概率就低
從上述特點可見,很難籠統地說哪種鎖更好,只能就具體應用的特點來說哪種鎖更合適!
僅從鎖的角度來說:表級鎖更適合于以查詢為主,只有少量按索引條件更新資料的應用,如Web 應用;
而行級鎖則更適合于有大量按索引條件并發更新少量不同資料,同時又有并查詢的應用,如一些在線事務處理(OLTP)系統,
MyISAM 表鎖
MyISAM 存盤引擎只支持表鎖,這也是MySQL開始幾個版本中唯一支持的鎖型別,
如何加表鎖
MyISAM 在執行查詢陳述句(SELECT)前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT 等)前,會自動給涉及的表加寫鎖,這個程序并不需要用戶干預,因此,用戶一般不需要直接用 LOCK TABLE 命令給 MyISAM 表顯式加鎖,
顯示加表鎖語法:
加讀鎖 : lock table table_name read;
加寫鎖 : lock table table_name write;
讀鎖案例
準備環境
create database demo_03 default charset=utf8mb4;
use demo_03;
CREATE TABLE `tb_book` (
`id` INT(11) auto_increment,
`name` VARCHAR(50) DEFAULT NULL,
`publish_time` DATE DEFAULT NULL,
`status` CHAR(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=myisam DEFAULT CHARSET=utf8 ;
INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'java編程思想','2088-08-01','1');
INSERT INTO tb_book (id, name, publish_time, status) VALUES(NULL,'solr編程思想','2088-08-08','0');
CREATE TABLE `tb_user` (
`id` INT(11) auto_increment,
`name` VARCHAR(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=myisam DEFAULT CHARSET=utf8 ;
INSERT INTO tb_user (id, name) VALUES(NULL,'令狐沖');
INSERT INTO tb_user (id, name) VALUES(NULL,'田伯光');
讀操作
客戶端一對book表加了鎖,并拿到了book表的鎖,在該鎖未釋放之前,不能去查別的表;
而客戶端二能查到book和其他表,是因為讀鎖是共享鎖,他并沒有真正拿到這把鎖,自然可以肆意妄為,不受未釋放鎖的束縛;
寫操作
- 客戶端①直接報錯,因為讀鎖會排斥寫操作
- 客戶端②陷入了阻塞狀態,得等待客戶端①釋放鎖
- unlock后,客戶端②的寫操作就能正常執行了,
總結
- 讀鎖對于加鎖的客戶端:會限制對其他表的查詢以及對任何表的寫操作
- 讀鎖對于其他客戶端:不會限制任何查詢,但會阻塞對該表的寫操作
??助記
自己拿到了讀鎖,那自己當然不能再去讀其他表,而又因為讀鎖不會影響到其他客戶端讀的結果,那其他客戶端自然可以任意讀,
而對于寫操作:自己還在讀,就別想著去做寫操作了!而對于其他客戶端,如果對該表寫操作,肯定會影響到當前客戶端的讀取結果,所以其他客戶端不能對該表進行寫操作
- 簡而言之:自己不能三心二意【操作其他表】,而對他人則考慮自己所做的操作會不會導致兩個客戶端拿到不一致的資料,會的話就是不允許的,
寫鎖案例
客戶端 一 :
1)獲得tb_book 表的寫鎖
lock table tb_book write ;
2)執行查詢操作
select * from tb_book ;
3)執行更新操作
update tb_book set name = 'java編程思想(第二版)' where id = 1;
更新操作執行成功 ;
客戶端二 :
4)執行查詢操作
select * from tb_book ;
- 陷入阻塞狀態,因為寫鎖是排他鎖,排斥其他客戶端的寫和讀操作,
當在客戶端一中釋放鎖指令 unlock tables 后 , 客戶端二中的 select 陳述句 就會立即執行
??總結
- 寫的優先級很高,對于鎖定的表可寫可讀,但同樣不能三心二意!!!而其他客戶端對于鎖定的表啥也干不了
結論
鎖模式的相互兼容性如表中所示:
由上表可見:
1) 對MyISAM 表的讀操作,不會阻塞其他用戶對同一表的讀請求,但會阻塞其他用戶對同一表的寫請求;
2) 對MyISAM 表的寫操作,則會阻塞其他用戶對同一表的讀和寫操作;
此外,MyISAM 的讀寫鎖調度是寫優先,這也是MyISAM不適合做寫為主的表的存盤引擎的原因,因為寫鎖后,其他執行緒不能做任何操作,大量的更新會使查詢很難得到鎖,從而造成永遠阻塞,
查看鎖的爭用情況
show open tables;
In_user : 表當前被查詢使用的次數,如果該數為零,則表是打開的,但是當前沒有被使用,
Name_locked:表名稱是否被鎖定,名稱鎖定用于取消表或對表進行重命名等操作,
show status like 'Table_locks%';
Table_locks_immediate : 指的是能夠立即獲得表級鎖的次數,每立即獲取鎖,值加1,
Table_locks_waited : 指的是不能立即獲取表級鎖而需要等待的次數,每等待一次,該值加1,此值高說明存在著較為嚴重的表級鎖爭用情況,
InnoDB 行鎖
行鎖介紹
行鎖特點 :偏向InnoDB 存盤引擎,開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,并發度也最高,
InnoDB 與 MyISAM 的最大不同有兩點:一是支持事務;二是采用了行級鎖,(兩者是息息相關的)
????事務
事務及其ACID屬性
事務是由一組SQL陳述句組成的邏輯處理單元,
事務具有以下4個特性,簡稱為事務ACID屬性,
| ACID屬性 | 含義 |
|---|---|
| 原子性(Atomicity) | 事務是一個原子操作單元,其對資料的修改,要么全部成功,要么全部失敗, |
| 一致性(Consistent) | 在事務開始和完成時,資料都必須保持一致狀態, |
| 隔離性(Isolation) | 資料庫系統提供一定的隔離機制,保證事務在不受外部并發操作影響的 “獨立” 環境下運行, |
| 持久性(Durable) | 事務完成之后,對于資料的修改是永久的, |
并發事務處理帶來的問題
| 問題 | 含義 |
|---|---|
| 丟失更新(Lost Update) | 當兩個或多個事務選擇同一行,最初的事務修改的值,會被后提交的事務修改的值覆寫, |
| 臟讀(Dirty Reads) | 讀到了另一個事務還未提交的資料 |
| 不可重復讀(Non-Repeatable Reads) | 一個事務執行同樣的兩次select陳述句,前后查詢出來的結果不一致 |
| 幻讀(Phantom Reads) | 一個事務按照相同的查詢條件重新讀取以前查詢過的資料,卻發現其他事務插入了滿足其查詢條件的新資料, |
幻讀
幻讀:就像出現了“幻影”一般,原本查不到這個人,然后要插入的時候,突然又說這個人存在
- 場景:注冊問題吧,查詢某個主鍵id是否存在,第一次查詢不存在,即將插入新資料時【剛好另一個人插入了該主鍵id】,導致這邊注冊失敗
- 幻讀在“當前讀”下才會出現,
- 幻讀僅專指“新插入的行”【update的不算】
??事務隔離級別
為了解決上述提到的事務并發問題,資料庫提供一定的事務隔離機制來解決這個問題,資料庫的事務隔離越嚴格,并發副作用越小,但付出的代價也就越大,因為事務隔離實質上就是使用事務在一定程度上“串行化” 進行,這顯然與“并發” 是矛盾的,
資料庫的隔離級別有4個,由低到高依次為Read uncommitted、Read committed、Repeatable read、Serializable,這四個級別可以逐個解決臟寫、臟讀、不可重復讀、幻讀這幾類問題,
| 隔離級別 | 丟失更新 | 臟讀 | 不可重復讀 | 幻讀 |
|---|---|---|---|---|
| Read uncommitted | × | √ | √ | √ |
| Read committed | × | × | √ | √ |
| Repeatable read(默認) | × | × | × | √ |
| Serializable(串行化) | × | × | × | × |
備注 : √ 代表可能出現 , × 代表不會出現 ,
- 讀未提交:別人修改了某行資料,還未提交我們就能看到,
- 讀已提交:別人修改了某行資料,得等到提交后我們才能看到, -- 解決臟讀
- 可重復讀:別人修改了某行資料,我們也不去讀那一行資料,還是讀我們當前事務最初的那個未被修改的值, -- 解決不可重復讀
- 串行化:對于同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”,當出現讀寫鎖沖突的時候,后訪問的事務必須等前一個事務執行完成,才能繼續執行,
例子
- 讀未提交:v1=v2=v3=2;B還未提交,A就可以看到了,
- 讀已提交:v1=1,v2=v3=2;等到B提交后,A才能看到,
- 可重復讀:v1=v2=1,v3=2;也就是說,所謂的可重復讀,是說在當前事務提交之前,只會讀取當前事務最初的值,而不去讀取其他的事務;
- 串行化:v1=1,v2=1,v3=2;事務A中查詢得到值1的時候,就會加了“讀鎖”,會阻塞其他事務對該行的寫操作(上文我們已經有提及到相關的讀鎖和寫鎖,忘記了的小伙伴可以翻閱勺ò復看)所以在事務B執行“將1改成2”的時候,會被鎖住,直到事務A提交后,事務B才可以繼續執行,
Mysql 的資料庫的默認隔離級別為 Repeatable read, 查看方式:
show variables like 'tx_isolation';
InnoDB 的行鎖模式
InnoDB 實作了以下兩種型別的行鎖,
- 共享鎖(S):又稱為讀鎖,簡稱S鎖,共享鎖就是多個事務對于同一資料可以共享一把鎖,都能訪問到資料,但是只能讀不能修改,
- 排他鎖(X):又稱為寫鎖,簡稱X鎖,排他鎖就是不能與其他鎖并存,如一個事務獲取了一個資料行的排他鎖,其他事務就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取排他鎖的事務是可以對資料就行讀取和修改,
對于UPDATE、DELETE和INSERT陳述句,InnoDB會自動給涉及資料集加**排他鎖**(X);
對于普通SELECT陳述句,InnoDB不會加任何鎖;
可以通過以下陳述句顯示給記錄集加共享鎖或排他鎖 ,
共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他鎖(X) :SELECT * FROM table_name WHERE ... FOR UPDATE (悲觀鎖)
即手動鎖定一行
悲觀鎖和樂觀鎖
悲觀鎖:事務必須排隊執行,資料鎖住了,不允許并發,(行級鎖:select后面添加for update)
樂觀鎖:支持并發,事務也不需要排隊,只不過需要一個版本號,
案例準備作業
create table test_innodb_lock(
id int(11),
name varchar(16),
sex varchar(1)
)engine = innodb default charset=utf8;
insert into test_innodb_lock values(1,'100','1');
insert into test_innodb_lock values(3,'3','1');
insert into test_innodb_lock values(4,'400','0');
insert into test_innodb_lock values(5,'500','1');
insert into test_innodb_lock values(6,'600','0');
insert into test_innodb_lock values(7,'700','0');
insert into test_innodb_lock values(8,'800','1');
insert into test_innodb_lock values(9,'900','1');
insert into test_innodb_lock values(1,'200','0');
create index idx_test_innodb_lock_id on test_innodb_lock(id);
create index idx_test_innodb_lock_name on test_innodb_lock(name);
行鎖基本演示
- 我們采用兩個客戶端,首先要關閉掉自動提交功能:set autocommit = 0;
- 普通的select不加鎖,沒有什么影響
- 而insert和update就不一樣了,會加排它鎖,其他客戶端陷入阻塞狀態,不能對該行(注意得兩個客戶端操作的是同一行,才會阻塞,因為是行鎖)進行修改,直到加鎖的客戶端提交完事務(相當于釋放鎖)
無索引行鎖升級為表鎖
如果不通過索引條件檢索資料,那么InnoDB將對表中的所有記錄加鎖,實際效果跟表鎖一樣,
由于 執行更新時 , name欄位本來為varchar型別, 我們是作為陣列型別使用,存在型別轉換,索引失效,最終行鎖變為表鎖 ;(字串型別,在SQL陳述句使用的時候沒有加單引號,導致索引失效,查詢沒有走索引,進行全表掃描,索引失效,行鎖就升級為表鎖)
InnoDB 行鎖爭用情況
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: 系統啟動后到現在總共等待的次數
當等待的次數很高,而且每次等待的時長也不小的時候,我們就需要分析系統中為什么會有如此多的等待,然后根據分析結果著手制定優化計劃,
總結
InnoDB存盤引擎由于實作了行級鎖定,雖然在鎖定機制的實作方面帶來了性能損耗可能比表鎖會更高一些,但是在整體并發處理能力方面要遠遠高于MyISAM的表鎖的,當系統并發量較高的時候,InnoDB的整體性能和MyISAM相比就會有比較明顯的優勢,
但是,InnoDB的行級鎖同樣也有其脆弱的一面,當我們使用不當的時候,可能會讓InnoDB的整體性能表現不僅不能比MyISAM高,甚至可能會更差,
優化建議
- 盡可能讓所有資料檢索都能通過索引來完成,避免無索引行鎖升級為表鎖,
- 合理設計索引,盡量縮小鎖的范圍,
- 盡可能減少索引條件,及索引范圍,避免間隙鎖,
- 盡量控制事務大小,減少鎖定資源量和時間長度,
- 盡可使用低級別事務隔離(但是需要業務層面滿足需求)
??表級鎖擴展
全域鎖
特點
備份的一致性問題
來看下邊這個場景,比如我們創建的購買操作,涉及到了用戶余額表+訂單表,流程順序如下:
- 當前正在備份用戶余額表,備份了小明同學的余額是100
- 此時小明剛好下了訂單,理應扣減50元
- 但由于用戶余額表已經備份完畢,余額表不會受到影響
- 小明下好單了,如今來備份訂單表了,能夠備份到小明剛下的單
到這里是否發現問題了,就是備份后的結果是:小明的余額沒扣錢,但卻有相關的訂單資料,出現了資料不一致的情況
- 那我們該如何規避這種現象呢?
1. 加全域鎖
通俗易懂,就是鎖住整個表,此時所有對資料的增刪改操作都會被阻塞
2. 不加鎖的一致性資料備份
上邊提到,備份時加上引數 --single-transaction就能實作此效果,具體是怎么做到的呢?
如果資料庫的引擎支持的事務支持可重復讀的隔離級別,那么在備份資料庫之前先開啟事務,會先創建 Read View,然后整個事務執行期間都在用這個 Read View,而且由于 MVCC 的支持,備份期間業務依然可以對資料進行更新操作,
即使其他事務更新了表的資料,也不會影響備份資料庫時的 Read View,這就是事務四大特性中的隔離性,這樣備份期間備份的資料一直是在開啟事務時的資料,
上文也提到了可重復讀,顧名思義就是,開啟事務后,無論其他事務是否更新了A資料,我們查到的依舊是開始事務時的原始A資料,而不會是更改后的,因此能保證在備份期間,即使有別的事務來更新,我們也不會備份到【進而就規避了資料不一致的情況】
元資料鎖
當存在事務,在對表的增刪查改陳述句時,其他事務若要改變表結構,會被阻塞,,
當有執行緒在執行 select 陳述句( 加 MDL 讀鎖)的期間,如果有其他執行緒要更改該表的結構( 申請 MDL 寫鎖),那么將會被阻塞,直到執行完 select 陳述句( 釋放 MDL 讀鎖),
反之,當有執行緒對表結構進行變更( 加 MDL 寫鎖)的期間,如果有其他執行緒執行了 CRUD 操作( 申請 MDL 讀鎖),那么就會被阻塞,直到表結構變更完成( 釋放 MDL 寫鎖),
兩者是互斥的,誰先來誰辦事,直到一方前者處理完畢
- MDL 不需要顯示呼叫,那它是在什么時候釋放的?
MDL 是在事務提交后才會釋放,這意味著事務執行期間,MDL 是一直持有的,
隱含問題
那如果資料庫有一個長事務(所謂的長事務,就是開啟了事務,但是一直還沒提交),那在對表結構做變更操作的時候,可能會發生意想不到的事情,比如下面這個順序的場景:
- 首先,執行緒 A 先啟用了事務(但是一直不提交),然后執行一條 select 陳述句,此時就先對該表加上 MDL 讀鎖;
- 然后,執行緒 B 也執行了同樣的 select 陳述句,此時并不會阻塞,因為「讀讀」并不沖突;
- 接著,執行緒 C 修改了表欄位,此時由于執行緒 A 的事務并沒有提交,也就是 MDL 讀鎖還在占用著,這時執行緒 C 就無法申請到 MDL 寫鎖,就會被阻塞
那么在執行緒 C 阻塞后,后續所有對該表的 select 陳述句,就都會被阻塞,如果此時有大量該表的 select 陳述句的請求到來,就會有大量的執行緒被阻塞住,這時資料庫的執行緒很快就會爆滿了,
- 為什么執行緒 C 因為申請不到 MDL 寫鎖,而導致后續的申請讀鎖的查詢操作也會被阻塞?
這是因為申請 MDL 鎖的操作會形成一個佇列,佇列中寫鎖獲取優先級高于讀鎖,一旦出現 MDL 寫鎖等待,會阻塞后續該表的所有 CRUD 操作,
如何解決
- 解決長事務,
為了能安全的對表結構進行變更,在對表結構變更前,先要看看資料庫中的長事務,是否有事務已經對表加上了 MDL 讀鎖,如果可以考慮 kill 掉這個長事務,然后再做表結構的變更,
- 對于熱點資料的表【kill掉后立馬又有長事務】
此時單單kill是沒用了,我們只能給這個alter陳述句設定等待時間,若超時未拿到MDL寫鎖,就放棄,不阻塞后續的select陳述句
??意向鎖
為什么要引入意向鎖
比如有兩個事務A跟B,和一個表G
A對G中的某一行加了行鎖,之后B要對G加表鎖的時候,行鎖跟表鎖就會產生沖突
- 為了解決沖突,B就需要遍歷全表,判斷是否有行鎖,這樣效率太低了,因此引入了意向鎖
如何解決
當A對G中的某一行加了行鎖后,會順便給表G加上意向鎖
- B要對G加表鎖的時候,只需要判斷表G的意向鎖,跟自己要加的表鎖是否兼容即可,無需再遍歷全表
意向鎖型別
意向鎖跟表鎖的兼容性
共享鎖的話,跟表鎖共享鎖兼容,但跟表鎖排它鎖是互斥的
排它鎖,自然都互斥
注意,意向鎖之間是兼容的,并且意向鎖不會與行級的共享鎖和排它鎖互斥
??AUTO-INC 鎖
資料庫的資料自增機制,就是基于這個鎖機制實作的,使得我們可以在insert的時候,不用指明資料的值,
AUTO-INC 鎖是特殊的表鎖機制,鎖不是在一個事務提交后才釋放,而是在執行完插入陳述句后就會立即釋放,【因此不遵循兩階段鎖協議(下文會提及到該協議)】
在插入資料時,會加一個表級別的 AUTO-INC 鎖,然后為被 AUTO_INCREMENT 修飾的欄位賦值遞增的值,等插入陳述句執行完成后,才會把 AUTO-INC 鎖釋放掉,
那么,一個事務在持有 AUTO-INC 鎖的程序中,其他事務的如果要向該表插入陳述句都會被阻塞,從而保證插入資料時,被 AUTO_INCREMENT 修飾的欄位的值是連續遞增的,
- 當然,這樣也有弊端
在對大量資料進行插入的時候,會影響插入性能,因為另一個事務中的插入會被阻塞,
因此, 在 MySQL 5.1.22 版本開始,InnoDB 存盤引擎提供了一種輕量級的鎖來實作自增,
一樣也是在插入資料的時候,會為被 AUTO_INCREMENT 修飾的欄位加上輕量級鎖,然后給該欄位賦值一個自增的值,就把這個輕量級鎖釋放了,而不需要等待整個插入陳述句執行完后才釋放鎖,
InnoDB 存盤引擎提供了個 innodb_autoinc_lock_mode 的系統變數,是用來控制選擇用 AUTO-INC 鎖,還是輕量級的鎖,
-
當 innodb_autoinc_lock_mode = 0,就采用 AUTO-INC 鎖;
-
當 innodb_autoinc_lock_mode = 2,就采用輕量級鎖;
-
當 innodb_autoinc_lock_mode = 1,這個是默認值,兩種鎖混著用,如果能夠確定插入記錄的數量就采用輕量級鎖,不確定時就采用 AUTO-INC 鎖,
-
自增值一旦分配了就會加一,即使回滾了,自增值也不會減一,而是繼續使用下一個值,所以自增值有可能不是連續的,
總結
- 常規的鎖住整個表,直到插入陳述句執行完畢后才釋放
- 為被 AUTO_INCREMENT修飾的欄位加上的輕量級鎖無需等到插入陳述句執行完畢后才釋放
??行級鎖擴展
??兩階段鎖協議
- 一個事務中,可能有多條陳述句,每條陳述句可能會加上鎖,那么這些鎖是什么時候才會釋放呢?
答案是:需要在事務commit之后才釋放,所以說,如果我們的事務中需要鎖多個行,要把盡可能粒度大的操作放到后邊!
行級鎖分類
- 行鎖(Record Lock) :單個行記錄上的鎖,
- 間隙鎖(Gap Lock) :鎖定一個范圍,不包括記錄本身,【解決幻讀現象】
- 臨鍵鎖(Next-key Lock) :Record Lock+Gap Lock【行鎖+間隙鎖】,鎖定一個范圍,包含記錄本身,行鎖只能鎖住已經存在的記錄,為了避免插入新記錄,需要依賴間隙鎖,
??間隙鎖&&臨鍵鎖
定義
間隙鎖:鎖定一個范圍,但不包含資料本身
臨鍵鎖:鎖定一個范圍,并且包含資料本身
對記錄加鎖時,加鎖的基本單位是 next-key lock,它是由記錄鎖和間隙鎖組合而成的,next-key lock 是左開右閉區間,而間隙鎖是左開右開區間,
假設一個索引包含值10、11、13和20,此索引可能的next-key鎖包括以下區間:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, ∞ ]
對于最后一個間隙,∞不是一個真正的索引記錄,因此,實際上,這個next-key鎖只鎖定最大索引值之后的間隙,
加鎖原則
兩個“原則”、兩個“優化”和一個“bug”,
- 原則1:加鎖的基本單位是next-key lock,
- 原則2:查找程序中訪問到的物件才會加鎖,
- 優化1:索引上的等值查詢,給唯一索引加鎖的時候,next-key lock退化為行鎖,
- 優化2:索引上的等值查詢,向右遍歷時且最后一個值不滿足等值條件的時候,next-key lock退化為間隙鎖,
- 一個bug:唯一索引上的范圍查詢會訪問到不滿足條件的第一個值為止,
??退化問題
但是,next-key lock 在一些場景下會退化成記錄鎖或間隙鎖,
案例準備
以下例子均在 MySQL 8.0.23版本下測驗
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【非唯一索引】 | d |
|---|---|---|
| 0 | 0 | 0 |
| 5 | 5 | 5 |
| 10 | 10 | 10 |
| 15 | 15 | 15 |
| 20 | 20 | 20 |
| 25 | 25 | 25 |
唯一索引等值查詢
- 若查詢的記錄【7】存在,則退化為記錄鎖,鎖的只是id為7這一個索引
- 若查詢的記錄不存在:
- 原則1:先統一加上next-key lock,(5,10];
- 再根據優化2,這是一個等值查詢(id=7),遍歷到最后發現id=10不滿足查詢條件,next-key lock退化成間隙鎖,因此最終加鎖的范圍是(5,10),
????理解
不要忘了我們引入間隙鎖的初衷,是為了解決幻讀現象,那這里我們是唯一索引:
-
如果查詢出來的id=7已經存在了,則不可能還會有其他事務能夠插入id為7的幻影進來,因為是唯一索引嘛,因此自然不需要再鎖間隙了,只需要鎖這一行就夠了,退化為行鎖
-
如果查詢出來的id=7不存在,相當于索引樹里邊還沒有7這個節點,我們要鎖住他,就只能通過他的相鄰節點5跟10,把這段區間鎖住
- 同時5跟10用不用鎖呢?我們這里是唯一索引,而且是7,不等于5也不等于10,所以5跟10不會影響到我們的7,不需要鎖,故只是鎖(5,10)
非唯一索引等值查詢
這里session A要給索引c上c=5的這一行加上讀鎖,
- 原則1,先加next-key lock,左開右閉,(0,5]
- 這里c是普通索引,不是唯一索引,所以不能確保只有當前c=5這一條記錄,還需要鎖住后邊的【因為后邊可能還會插入c=5】,因此還需要向后遍歷,直到c=10這條記錄,訪問到的都要加鎖【原則2】,(5,10]
- 優化2:等值判斷,向右遍歷,最后一個值不滿足c=5這個等值條件,因此退化成間隙鎖(5,10),
因此sessionC的操作會被阻塞,這是可以理解的,那sessionB呢?為什么不會被阻塞呢?
- 根據原則2 ,只有訪問到的物件才會加鎖,這個查詢使用覆寫索引,并不需要訪問主鍵索引,所以主鍵索引上沒有加任何鎖,因此sessionC不會被阻塞,
鎖的是索引
在這個例子中,lock in share mode只鎖覆寫索引,但是如果是for update就不一樣了, 執行 for update時,系統會認為你接下來要更新資料,因此會順便給主鍵索引上滿足條件的行加上行鎖,
同時,如果你要用lock in share mode來給行加讀鎖避免資料被更新的話,就必須得繞過覆寫索引的優化,在查詢欄位中加入索引中不存在的欄位,
比如,將session A的查詢陳述句改成 select d from t where c=5 lock in share mode,
這樣就不得不回表,就會涉及到主鍵索引了【其實就是讓覆寫索引失效】
唯一索引范圍鎖
- 等值查詢,先給10加上間隙鎖,(5,10]
- 優化1:退化成行鎖,只鎖10這一行
- 由于是范圍查詢,繼續往后遍歷,直到15這一行停下來,訪問到的都要加next-key lock,(10,15]
- 由于15不滿足查詢條件,故會退化為間隙鎖,(10,15)
因此最后的范圍是[10,15),sessionB的第二條insert會被阻塞,其他都不會
非唯一索引范圍查詢
跟唯一索引范圍鎖的區別在于,普通索引中的next-key lock不會退化為間隙鎖和記錄鎖
- next-key lock,(5,10],由于c不是唯一索引,所以不會退化為行鎖
- 繼續往后遍歷,直到15,next-key lock,(10,15]
因此最后的范圍是:(5,15],兩條陳述句都會被阻塞
??非索引查詢
如果使用的是沒有索引的欄位,比如update user set age=7 where name=‘xxx(即使沒有匹配到任何資料)’,那么會給全表加入gap鎖,同時,它不能像上文中行鎖一樣經過MySQL Server過濾自動解除不滿足條件的鎖,因為沒有索引,則這些欄位也就沒有排序,也就沒有區間,除非該事務提交,否則其它事務無法插入任何資料,
??死鎖
間隙鎖死鎖
間隙鎖潛在問題
注意,間隙鎖與間隙鎖之間是不會沖突的
- session A 執行 select … for update 陳述句,由于 id=9 這一行并不存在,因此會加上間隙鎖 (5,10);
- session B 執行 select … for update 陳述句,同樣會加上間隙鎖 (5,10),間隙鎖之間不會沖突,因此這個陳述句可以執行成功;
- session B 試圖插入一行 (9,9,9),被 session A 的間隙鎖擋住了,只好進入等待;
- session A 試圖插入一行 (9,9,9),被 session B 的間隙鎖擋住了,
- 兩個 session 進入互相等待狀態,形成死鎖,此時我們來看如何應對死鎖...
解決死鎖方案
- 直接進入等待,直到超時,這個超時時間可以通過引數 innodb_lock_wait_timeout 來設定,
- 發起死鎖檢測,發現死鎖后,主動回滾死鎖鏈條中的某一個事務,讓其他事務得以繼續執行,將引數 innodb_deadlock_detect 設定為 on,表示開啟這個邏輯,
如果選用第一種策略,其實不好估量,我們不確定這個超時時間要設定為多少合適,因此一般使用第二種策略,
但是它也是有額外負擔的,
每當一個事務被鎖的時候,就要看看它所依賴的執行緒有沒有被別人鎖住,如此回圈,最后判斷是否出現了回圈等待【死鎖產生的條件之一】,也就是死鎖,
每個新來的被堵住的執行緒,都要判斷會不會由于自己的加入導致了死鎖,這是一個時間復雜度是 O(n) 的操作,
在作業系統里邊,應對死鎖的最好方法是:預防死鎖的產生hhh,這個預防,可能很難跟我們開發工程師牽扯上,更多涉及到DBA那邊了,
常見的解決死鎖的方法
1、如果不同程式會并發存取多個表,盡量約定以相同的順序訪問表,可以大大降低死鎖機會,
2、在同一個事務中,盡可能做到一次鎖定所需要的所有資源,減少死鎖產生概率;
3、對于非常容易產生死鎖的業務部分,可以嘗試使用升級鎖定顆粒度,通過表級鎖定來減少死鎖產生的概率;
如果業務處理不好可以用分布式事務鎖或者使用樂觀鎖
總結
還是那張腦圖,再看一遍,嘗試復述出來,就過關啦
??下篇預告
這篇我們主要講的是鎖相關的知識,事務只是入了門,關于事務背后的原理,以及MVCC多版本并發控制,這些我們留到后邊再來詳解,
??參考文獻
- 小林coding
- MySQL45講
- 黑馬MySQL視頻
收藏=白嫖,點贊+關注才是真愛!!!本篇文章如有不對之處,還請在評論區指出,歡迎添加我的微信一起交流:Melo__Jun
??友鏈
-
MySQL高級篇專欄
-
??我的一年后臺練習生涯
-
聊聊Java
-
分布式開發實戰
-
Redis入門與實戰
-
資料結構與演算法
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/523140.html
標籤:MySQL
