這一節,咱們來聊一下mysql鎖的內容。mysql鎖是爲了協調多個用戶訪問同一個資源,保障併發時的一致性和有效性。mysql
按照鎖的範圍劃分,咱們能夠分爲sql
全局鎖是在整個數據庫加上讀鎖。讓數據庫處於只讀狀態,別的進程執行一下命令會阻塞住:數據更新語句(增刪改查),數據定義語句(建表,表結構修改)和更新類事務的提交語句。數據庫
全局鎖的語句爲:安全
FLUSH TABLES WITH READ LOCK;
複製代碼
簡稱 FTWRL, 解鎖語句爲:bash
UNLOCK TABLES;
複製代碼
全局鎖使用場景是作全庫備份,讓整個庫只讀,這聽上去很危險session
那麼爲何還要使用全局鎖呢?這是爲了防止數據不一致。舉個栗子: 好比一個購物網站,其中有兩張表 account(帳戶表)和order(訂單表),咱們下了總價100元的訂單,操做步驟以下:併發
如今咱們不使用全局鎖備份數據。在備份時,正好有人下了個100元的訂單,那麼備份出來的數據有以下幾種狀況:工具
也就是說若是不加鎖,備份的數據會可能不會在同一個邏輯點,數據的邏輯是不一致的。那麼有沒有更好的備份方案呢,既能夠備份且保持數據一致性,又能夠不影響業務運行?還真有這樣一個方案:測試
使用官方自帶的mysqldump工具,使用時加上--single-transaction。
複製代碼
使用時會在啓動一個事務,來保證一致性。因爲MVCC的支持,這個過程是能夠正常更新的,不用停業務。網站
固然此方法僅支持帶事務功能的存儲引擎,MyISAM引擎就不支持。
全局鎖目的
全局鎖使用場景:
語句爲:
當存儲引擎支持事務時,可使用以下工具作替換方案
表級鎖,顧名思義就是鎖住整張表,MYSQL中表鎖分爲兩張狀況:表鎖和原數據鎖(MDL)。
表鎖又分爲表讀鎖和表寫鎖。
表讀鎖語句:
LOCK TABLES [tablename] READ
複製代碼
表寫鎖的語句:
LOCK TABLES [tablename] WRITE
複製代碼
解鎖語句:
UNLOCK TABLES
複製代碼
在還沒出現更細的鎖顆粒度時,咱們經常使用來處理併發狀況,但在InnerDB這種數據引擎下通常不用。
咱們先新建一個數據庫:
CREATE TABLE `t16` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) NOT NULL COMMENT '惟一索引',
`b` int(11) NOT NULL,
`c` int(11) NOT NULL COMMENT '普通索引',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_a` (`a`) USING BTREE,
KEY `idx_c` (`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
複製代碼
再插入一條紀錄:
INSERT INTO `t16`(`a`,`b`,`c`) VALUES(1,1,1);
複製代碼
咱們先來看一下研究一下讀鎖:
事務A | 事務B |
---|---|
LOCK TABLES t16 READ; Query OK, 0 rows affected (0.00 sec) 給表加讀鎖 |
|
SELECT * FROM t16; 1 row in set (0.00 sec) 正常返回結果 |
SELECT * FROM t16; 1 row in set (0.00 sec) 正常返回結果 |
INSERT INTO t16(a,b,c) VALUES(2,2,2); ERROR 1099 (HY000): Table 't16' was locked with a READ lock and can't be updated 報錯 |
INSERT INTO t16(a,b,c) VALUES(3,3,3);鎖住 |
UNLOCK TABLES; Query OK, 0 rows affected (0.00 sec) 解鎖 |
Query OK, 1 row affected (1 min 18.19 sec)寫入成功 |
咱們再來研究一下寫鎖:
事務A | 事務B |
---|---|
LOCK TABLES t16 WRITE; Query OK, 0 rows affected (0.00 sec) 給表加寫鎖 |
|
SELECT * FROM t16; 2 row in set (0.00 sec) 正常返回結果 |
SELECT * FROM t16;等待 |
UNLOCK TABLES;解鎖 |
2 rows in set (58.85 sec)正常返回結果 |
LOCK TABLES t16 WRITE; Query OK, 0 rows affected (0.00 sec) 給表加寫鎖 |
|
INSERT INTO t16(a,b,c) VALUES(2,2,2); Query OK, 1 row affected (0.00 sec) 正常 |
INSERT INTO t16(a,b,c) VALUES(4,4,4);鎖住 |
UNLOCK TABLES; Query OK, 0 rows affected (0.00 sec) 解鎖 |
Query OK, 1 row affected (1 min 18.19 sec)寫入成功 |
根據上面的栗子,咱們能夠得出下面這個邏輯:
本事務讀 | 本事務寫 | 其餘事務讀 | 其餘事務寫 | |
---|---|---|---|---|
加讀鎖後 | 正常 | 報錯 | 正常 | 等待 |
加寫鎖後 | 正常 | 正常 | 等待 | 等待 |
MDL不須要顯式使用,在訪問一個表的時候會被自動加上。MDL的做用是,保證讀寫的正確性。你能夠想象一下,若是一個查詢正在遍歷一個表中的數據,而執行期間另外一個線程對這個表結構作變動,刪了一列,那麼查詢線程拿到的結果跟表結構對不上,確定是不行的。 所以,在 MySQL 5.5 版本中引入了 MDL,當對一個表作增刪改查操做的時候,加 MDL 讀鎖;當要對錶作結構變動操做的時候,加 MDL 寫鎖。
咱們在工做中,不少時候須要考慮MDL的存在,不然可能致使鎖等待或者鏈接長時間打滿的狀況。咱們來看下面的栗子:
session1 | session2 | session3 |
---|---|---|
SELECT a,b,SLEEP(60) FROM t16; | ||
ALTER TABLE t16 ADD COLUMN f int; | ||
SELECT * FROM t16 WHERE id=1; | ||
4 rows in set (4 min 0.02 sec)4條紀錄因此4分鐘後返回結果 |
Query OK, 0 rows affected (3 min 57.35 sec)session1執行完後當即執行 |
1 row in set (3 min 35.10 sec)session2執行完後當即執行 |
session1中又一條慢sql須要100s返回,他致使了session2中的修改表結構的語句阻塞,而session2中的語句又致使了,其餘session中的語句的阻塞。因此短期內數據庫鏈接很容易被打滿了。那要怎麼作呢?可使用kill語句來強制結束session1或session2中的語句。 所以對於開發來講,在工做中應該儘可能避免慢查詢、儘可能保證事務及時提交、避免大事務等,固然對於 DBA 來講,也應該儘可能避免在業務高峯執行 DDL 操做。
標級鎖分爲表鎖和原數據鎖(MDL) 表級別讀鎖和寫鎖的區別爲
元數據鎖
** 在Innodb中,行鎖是在須要時才加上,在事務提交時解鎖,這就是兩階段鎖的協議。**
咱們知道這個設定對咱們使用事務有什麼幫助呢? 若是你的事務要鎖多行,須要把最可能形成鎖衝突的鎖日後放 舉個栗子: A顧客須要在電影院B買票,咱們簡化一個流程:
若是單從鎖影響併發的方面考慮,應該若是規劃他們之間的順序呢?
根據兩階段鎖協議,不論你怎樣安排語句順序,全部的操做須要的行鎖都是在事務提交的時候才釋放的。因此,若是你把語句 2 安排在最後,好比按照 三、一、2 這樣的順序,那麼影院帳戶餘額這一行的鎖時間就最少。這就最大程度地減小了事務之間的鎖等待,提高了併發度。
Innodb的鎖按照功能分,能夠分爲共享鎖(讀鎖)和排他鎖(寫鎖)。
對於普通 select 語句,InnoDB 不會加任何鎖,事務能夠經過如下語句顯式給記錄集加共享鎖或排他鎖:
接下來咱們分析一下RC隔離級別下的鎖的狀況。 咱們分爲三種狀況
測試表咱們沿用上面的t16表,其中a惟一索引,b無索引,c非惟一索引。清空表數據,並在表中加入幾條數據
truncate table t16;
insert into t16(a,b,c) values (1,1,1),(2,2,2),(3,3,3),(4,4,3);
複製代碼
session1 | session2 |
---|---|
set session transaction_isolation='READ-COMMITTED';/* 設置會話隔離級別爲 RC*/ | set session transaction_isolation='READ-COMMITTED';/* 設置會話隔離級別爲 RC*/ |
begin; | begin; |
SELECT * FROM t16 WHERE b=1 for update;正常 |
|
SELECT * FROM t16 WHERE b=3 for update;等待 |
|
commit; | session1結束,結果正常返回 |
commit; |
表面看來,session1只給b=1加了排他鎖,實際在沒有索引的清空下,他給整張表加了排他鎖。下圖是加鎖的邏輯圖:
沒有索引的狀況下,InnoDB 的當前讀會對全部記錄都加鎖。因此在工做中應該特別注意 InnoDB 這一特性,不然可能會產生大量的鎖衝突。
session1 | session2 |
---|---|
set session transaction_isolation='READ-COMMITTED';/* 設置會話隔離級別爲 RC*/ | set session transaction_isolation='READ-COMMITTED';/* 設置會話隔離級別爲 RC*/ |
begin; | begin; |
SELECT * FROM t16 WHERE a=1 for update;正常 |
|
SELECT * FROM t16 WHERE a=2 for update;正常 |
|
SELECT * FROM t16 WHERE c=1 for update;等待 |
|
commit; | session1結束,結果正常返回 |
commit; |
session1 給了 a=1 這一行加了排他鎖,在 session2 中請求其餘行的排他鎖時,不會發生等待;可是在 session2 中請求 a=1 這一行的排他鎖時,會發生等待。看下圖:
若是查詢的條件是惟一索引,那麼 SQL 須要在知足條件的惟一索引上加鎖,而且會在對應的聚簇索引上加鎖。
session1 | session2 |
---|---|
set session transaction_isolation='READ-COMMITTED';/* 設置會話隔離級別爲 RC*/ | set session transaction_isolation='READ-COMMITTED';/* 設置會話隔離級別爲 RC*/ |
begin; | begin; |
SELECT * FROM t16 WHERE c=3 for update;正常 |
|
SELECT * FROM t16 WHERE a=2 for update;正常 |
|
SELECT * FROM t16 WHERE a=4 for update;等待 |
|
commit; | session1結束,結果正常返回 |
commit; |
咱們在知足條件 c=3 的數據上加了排他鎖,如上面結果,就是第 三、4 行。所以第 一、2 行的數據沒被鎖,而 三、4 行的數據被鎖了。以下圖:
若是查詢的條件是非惟一索引,那麼 SQL 須要在知足條件的非惟一索引上都加上鎖,而且會在它們對應的聚簇索引上加鎖。
在研究RR隔離級別下,咱們先來看一道小題目:
session1 | session2 |
---|---|
set session transaction_isolation='READ-COMMITTED';/* 設置會話隔離級別爲 RC*/ | set session transaction_isolation='READ-COMMITTED';/* 設置會話隔離級別爲 RC*/ |
begin; | begin; |
SELECT * FROM t16 WHERE c=3 for update;Result1 |
|
INSERT INTO t16(a,b,c) VALUES(5,5,3); | |
commit; | |
SELECT * FROM t16 WHERE c=3 for update;Result1 |
|
commit; |
Result1和Result2分別是多少?
咱們通過測試Result1爲:
咱們通過測試Result2爲:
咱們發現Result2比Result1多了一行,這也就是咱們所講的幻讀。
那爲何會出現幻讀,咱們來看下面一張圖:
從圖中能夠看出,RC 隔離級別下,只鎖住了知足 c=3 的當前行,而不會對後面的位置(或者說間隙)加鎖,所以致使 session1 的寫入語句能正常執行並提交。
爲了解決幻讀問題,RR隔離級別引入了間隙鎖。
咱們從新建一張表,並插入數據:
CREATE TABLE `t17` (
`id` int(11) NOT NULL,
`a` int(11) NOT NULL COMMENT '惟一索引',
`b` int(11) NOT NULL,
`c` int(11) NOT NULL COMMENT '普通索引',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_a` (`a`) USING BTREE,
KEY `idx_c` (`c`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
INSERT INTO t17(id, a, b, c) VALUES(1,1,1,1),(2,2,2,2),(4,4,4,4),(6,6,6,4);
複製代碼
咱們同樣分爲三種狀況:
session1 | session2 | session3 |
---|---|---|
set session transaction_isolation='REPEATABLE-READ';/* 設置會話隔離級別爲 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 設置會話隔離級別爲 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 設置會話隔離級別爲 RR*/ |
begin; | begin; | begin; |
SELECT * FROM t17 WHERE b=1 for update;正常 |
||
SELECT * FROM t17 WHERE a=4 for update;等待 |
||
INSERT INTO t17(id, a, b, c) VALUES(5,5,5,5);等待 |
||
commit; | session1結束,結果正常返回 |
session1結束,結果正常返回 |
rollback; | rollback; |
其加鎖的邏輯圖以下:
如圖,全部記錄都有 X 鎖,除此以外,每一個 GAP 也被加上了 GAP 鎖。所以這張表在執行完 select * from t17 where b=1 for update; 到 commit 以前,除了不加鎖的快照讀,其它任何加鎖的 SQL,都會等待,若是這是線上業務表,那就是件很是恐怖的事情了。
RR 隔離級別下,非索引字段作條件的當前讀不但會把每條記錄都加上 X 鎖,還會把每一個 GAP 加上 GAP 鎖。再次說明,條件字段加索引的重要性。
session1 | session2 | session3 |
---|---|---|
set session transaction_isolation='REPEATABLE-READ';/* 設置會話隔離級別爲 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 設置會話隔離級別爲 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 設置會話隔離級別爲 RR*/ |
begin; | begin; | begin; |
SELECT * FROM t17 WHERE c=4 for update;正常 |
||
SELECT * FROM t17 WHERE a=4 for update;等待 |
||
INSERT INTO t17(id, a, b, c) VALUES(5,5,5,4);等待 |
||
commit; | session1結束,結果正常返回 |
session1結束,結果正常返回 |
rollback; | rollback; |
其加鎖的邏輯圖以下:
與 RC 隔離級別下的圖類似,可是有個比較大的區別是:RR 隔離級別多了 GAP 鎖。
如上圖,首先須要考慮哪些位置能夠插入新的知足條件 c=4 的項:
爲了保證這幾個區間不會插入新的知足條件 c=4 的記錄,MySQL RR 隔離級別選擇了 GAP 鎖,將這幾個區間鎖起來。 而上面,咱們插入了(id=5, c=4)的數據,因此被鎖住了。
RR 隔離級別下,非索引字段作條件的當前讀不但會把每條記錄都加上 X 鎖,還會把每一個 GAP 加上 GAP 鎖。再次說明,條件字段加索引的重要性。
所以以惟一索引爲條件的當前讀,不會有 GAP 鎖。因此 RR 隔離級別下的惟一索引當前讀加鎖狀況與 RC 隔離級別下的惟一索引當前讀加鎖狀況一致。這裏就再也不實驗了。
session1 | session2 |
---|---|
set session transaction_isolation='REPEATABLE-READ';/* 設置會話隔離級別爲 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 設置會話隔離級別爲 RR*/ |
begin; | begin; |
SELECT * FROM t17 WHERE c=4 for update;正常 |
|
INSERT INTO t17(id, a, b, c) VALUES(5,5,5,5);等待 |
|
commit; | session1結束,結果正常返回 |
rollback; |
咱們發現插入語句居然等待了,爲何? 由於間隙鎖鎖的是位置,根據上面這張圖,session1的sql語句鎖的範圍是 「(2,無窮大]」,致使凡在這個區間都會被鎖住。
因此間隙鎖的引入會致使鎖的範圍更大,影響併發。 有不少公司使用RC隔離級別+日誌ROW模式。
1)行級鎖是兩階段的鎖,一個事務中在須要時鎖住,在commit時釋放鎖。
2)行級鎖按照功能分爲共享鎖和排他鎖。
3)在RC隔離級別下,會對數據加上紀錄鎖,但會有幻讀的問題
4)在RR隔離級別下,會對數據加上紀錄鎖和間隙鎖,解決了幻讀的問題,但影響併發。
死鎖是指兩個或者多個事務在同一資源上相互佔用,並請求鎖定對方佔用的資源,從而致使惡性循環的現象。 咱們使用t17表來舉個栗子,看如何會產生死鎖:
session1 | session2 |
---|---|
set session transaction_isolation='REPEATABLE-READ';/* 設置會話隔離級別爲 RR*/ | set session transaction_isolation='REPEATABLE-READ';/* 設置會話隔離級別爲 RR*/ |
begin; | begin; |
SELECT * FROM t17 WHERE c=4 for update;sql1正常 |
SELECT * FROM t17 WHERE c=1 for update;sql2正常 |
INSERT INTO t17(id, a, b, c) VALUES(5,5,5,5);sql3等待 |
|
INSERT INTO t17(id, a, b, c) VALUES(7,7,7,1);sql4報死鎖錯誤 |
咱們來分析一下,剛開始執行sql1時 session1鎖住的範圍時 (2,正無窮],執行sql2後session2鎖住範圍時(負無窮大,2) , sql3範圍在session1鎖的範圍內,因此等待session1鎖釋放,而此時,sql4又在等待session2的釋放。也就是session1和sesison2相互等待各自釋放。因此就成了死鎖了。
InnoDB 中解決死鎖問題有兩種方式:
通常咱們採用第一種方案,由於第二種方案等待50秒時間過長,業務上沒法接受,若是把時間調少,好比1秒,又有可能會誤殺一些正常的鎖。但第一種方案也會形成額外的cpu開銷。
1) 死鎖是指兩個或者多個事務在同一資源上相互佔用,並請求鎖定對方佔用的資源,從而致使惡性循環的現象。 2) 咱們通常使用InnoDB自帶的檢索機制來檢索是否死鎖。
有三張表,用戶帳戶表,商品庫存表,訂單表。如今用戶須要買商品,購買流程是
咱們改如何操做這些表來完成上面的購買流程,而且支持更多的併發?