公司內部分享之mysql鎖

mysql鎖

這一節,咱們來聊一下mysql鎖的內容。mysql鎖是爲了協調多個用戶訪問同一個資源,保障併發時的一致性和有效性。mysql

按照鎖的範圍劃分,咱們能夠分爲sql

  • 全局鎖
  • 表鎖
  • 行鎖

全局鎖

全局鎖是在整個數據庫加上讀鎖。讓數據庫處於只讀狀態,別的進程執行一下命令會阻塞住:數據更新語句(增刪改查),數據定義語句(建表,表結構修改)和更新類事務的提交語句。數據庫

全局鎖的語句爲:安全

FLUSH TABLES WITH READ LOCK;
複製代碼

簡稱 FTWRL, 解鎖語句爲:bash

UNLOCK TABLES;
複製代碼

全局鎖使用場景是作全庫備份,讓整個庫只讀,這聽上去很危險session

  • 在主庫備份,會致使數據沒法寫入,業務停擺
  • 在從庫備份,從庫不能同步binlog中的日誌,會導從庫數據延遲。

那麼爲何還要使用全局鎖呢?這是爲了防止數據不一致。舉個栗子: 好比一個購物網站,其中有兩張表 account(帳戶表)和order(訂單表),咱們下了總價100元的訂單,操做步驟以下:併發

  • 1)account表中減去100
  • 2)order表中新增一個價值100的訂單

如今咱們不使用全局鎖備份數據。在備份時,正好有人下了個100元的訂單,那麼備份出來的數據有以下幾種狀況:工具

  • 1)account表和order表同時備份到(運氣真好,正常)
  • 2)account表和order表都沒備份到 (下單失敗,沒賺也沒損失,能接受)
  • 3)account表沒備份,order表備份(沒花錢,白賺了100元的物品,商家哭暈在廁所)
  • 4)account表備份,order表沒備份(花了錢,沒訂單,投訴去,商家又哭暈在廁所)

也就是說若是不加鎖,備份的數據會可能不會在同一個邏輯點,數據的邏輯是不一致的。那麼有沒有更好的備份方案呢,既能夠備份且保持數據一致性,又能夠不影響業務運行?還真有這樣一個方案:測試

使用官方自帶的mysqldump工具,使用時加上--single-transaction。
複製代碼

使用時會在啓動一個事務,來保證一致性。因爲MVCC的支持,這個過程是能夠正常更新的,不用停業務。網站

固然此方法僅支持帶事務功能的存儲引擎,MyISAM引擎就不支持。

小結:

全局鎖目的

  • 保證數據一致性。

全局鎖使用場景:

  • 在備份庫時使用

語句爲:

  • FLUSH TABLES WITH READ LOCK; //加鎖
  • UNLOCK TABLES; // 解鎖

當存儲引擎支持事務時,可使用以下工具作替換方案

  • mysqldump工具加--single-transaction

表級鎖

表級鎖,顧名思義就是鎖住整張表,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不須要顯式使用,在訪問一個表的時候會被自動加上。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) 表級別讀鎖和寫鎖的區別爲

  • 加讀鎖,本事務可正常讀,寫報錯,其餘事務可正常讀,寫等待。
  • 加寫鎖,本事務正常讀寫,其餘事務讀寫等待。

元數據鎖

  • 在修改表結構時,會加上MDL寫鎖。
  • 儘可能不要在高峯期執行修改表結構的語句,容易引發鏈接打滿。

行級鎖

兩階段鎖

** 在Innodb中,行鎖是在須要時才加上,在事務提交時解鎖,這就是兩階段鎖的協議。**

咱們知道這個設定對咱們使用事務有什麼幫助呢? 若是你的事務要鎖多行,須要把最可能形成鎖衝突的鎖日後放 舉個栗子: A顧客須要在電影院B買票,咱們簡化一個流程:

  1. A顧客帳戶扣錢
  2. B電影院帳戶加錢
  3. 插入一條日誌紀錄

若是單從鎖影響併發的方面考慮,應該若是規劃他們之間的順序呢?

根據兩階段鎖協議,不論你怎樣安排語句順序,全部的操做須要的行鎖都是在事務提交的時候才釋放的。因此,若是你把語句 2 安排在最後,好比按照 三、一、2 這樣的順序,那麼影院帳戶餘額這一行的鎖時間就最少。這就最大程度地減小了事務之間的鎖等待,提高了併發度。

共享鎖和排他鎖

Innodb的鎖按照功能分,能夠分爲共享鎖(讀鎖)和排他鎖(寫鎖)。

  • 共享鎖:容許一個事務去讀一行,阻止其它事務得到相同數據集的排他鎖。
  • 排他鎖:容許得到排他鎖的事務更新數據,阻止其它事務取得相同數據集的共享讀鎖和排他寫鎖。

對於普通 select 語句,InnoDB 不會加任何鎖,事務能夠經過如下語句顯式給記錄集加共享鎖或排他鎖:

  • 共享鎖:select * from table_name where … lock in share mode;
  • 排他鎖:select * from table_name where … for update;
  • update,delete 語句也會加上排他鎖

RC隔離級別下的鎖

接下來咱們分析一下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隔離級別下的鎖

在研究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 的項:

  • 因爲 B+ 樹索引是有序的,所以 [2,2](表明 c 和 id 的值,後面就不一一說明了)前面的記錄,不可能插入 c=4 的記錄了;
  • [2,2] 與 [4,4] 之間能夠插入 [4,3];
  • [4,4] 與 [4,6] 之間能夠插入 [4,5];
  • [4,6] 以後,能夠插入的值就不少了:[4,n](其中 n>6) ;

爲了保證這幾個區間不會插入新的知足條件 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 中解決死鎖問題有兩種方式:

  • 檢測到死鎖的循環依賴,當即返回一個錯誤(這個報錯內容請看下面的實驗),將參數 innodb_deadlock_detect 設置爲 on 表示開啓這個邏輯;
  • 等查詢的時間達到鎖等待超時的設定後放棄鎖請求。這個超時時間由 innodb_lock_wait_timeout 來控制。默認是 50 秒。

通常咱們採用第一種方案,由於第二種方案等待50秒時間過長,業務上沒法接受,若是把時間調少,好比1秒,又有可能會誤殺一些正常的鎖。但第一種方案也會形成額外的cpu開銷。

下降死鎖的方案:

  • 更新 SQL 的 where 條件儘可能用索引;
  • 基於 primary 或 unique key 更新數據;
  • 減小範圍更新,尤爲非主鍵、非惟一索引上的範圍更新;
  • 加鎖順序一致,儘量一次性鎖定全部須要行;
  • 將 RR 隔離級別調整爲 RC 隔離級別。

小結

1) 死鎖是指兩個或者多個事務在同一資源上相互佔用,並請求鎖定對方佔用的資源,從而致使惡性循環的現象。 2) 咱們通常使用InnoDB自帶的檢索機制來檢索是否死鎖。

小練習:

有三張表,用戶帳戶表,商品庫存表,訂單表。如今用戶須要買商品,購買流程是

  • 用戶帳戶扣除相應金額,若是金額不夠購買失敗。
  • 商品庫存扣除相應庫存,若是庫存不夠購買失敗。
  • 訂單表新增一個訂單紀錄。

咱們改如何操做這些表來完成上面的購買流程,而且支持更多的併發?

參考文檔

相關文章
相關標籤/搜索