MySql各種鎖機制的學習(摘抄及總結) 鎖的分類

內容來之以下博客:
https://blog.csdn.net/qq_38238296/article/details/88362999
https://www.cnblogs.com/zhoulujun/p/11710318.html
https://zhuanlan.zhihu.com/p/48269420
https://segmentfault.com/a/1190000014133576

加鎖的目的:

數據庫是一個多用戶使用的共享資源。當多個用戶併發地存取數據時,在數據庫中就會產生多個事務同時存取同一數據的情況。若對併發操作不加控制就可能會讀取和存儲不正確的數據,破壞數據庫的一致性。鎖是用於管理對公共資源的併發控制。也就是說在併發的情況下,會出現資源競爭,所以需要加鎖。
加鎖解決了 多用戶環境下保證數據庫完整性和一致性。
Lock的對象是事務,用來鎖定的是數據庫中的對象,如表、頁、行。並且一般lock的對象僅在事務commit或rollback後進行釋放(不同事務隔離級別釋放的時間可能不同)。

鎖的分類

鎖粒度

一、行鎖

行級鎖是Mysql中鎖定粒度最細的一種鎖,表示只針對當前操作的行進行加鎖。行級鎖能大大減少數據庫操作的衝突。其加鎖粒度最小,但加鎖的開銷也最大。有可能會出現死鎖的情況。 行級鎖按照使用方式分爲共享鎖和排他鎖。

二、表鎖

​表級鎖是mysql鎖中粒度最大的一種鎖,表示當前的操作對整張表加鎖,資源開銷比行鎖少,不會出現死鎖的情況,但是發生鎖衝突的概率很大。被大部分的mysql引擎支持,MyISAM和InnoDB都支持表級鎖,但是InnoDB默認的是行級鎖。
Mysql的表級別鎖分爲兩類:元數據鎖(Metadata Lock,MDL)、表鎖。

元數據鎖(Metadata Lock,MDL)

元數據鎖(MDL) 不需要顯式使用,在訪問一個表的時候會被自動加上。這個特性需要MySQL5.5版本以上纔會支持,當對一個表做增刪改查的時候,該表會被加MDL讀鎖當對錶做結構變更的時候,加MDL寫鎖

  • MDL鎖規則:
    1、讀鎖之間不互斥
    2、讀寫鎖、寫鎖之間是互斥的,爲了保證表結構變更的安全性,所以如果要多線程對同一個表加字段等表結構操作,就會變成串行化,需要進行鎖等待
    3、MDL的寫鎖優先級比MDL讀鎖的優先級
    4、MDL的鎖釋放必須要等到事務結束纔會釋放
MDL鎖的例子

若沒有MDL鎖的保護,則事務2可以直接執行DDL操作,並且導致事務1出錯,5.1版本即是如此。5.5版本加入MDL鎖就在於保護這種情況的發生,由於事務1開啓了查詢,那麼獲得了MDL鎖,鎖的模式爲SHARED_READ,事務2要執行DDL,則需獲得EXCLUSIVE鎖,兩者互斥,所以事務2需要等待。

頁鎖

​ 頁級鎖是MySQL中鎖定粒度介於行級鎖和表級鎖中間的一種鎖。表級鎖速度快,但衝突多,行級衝突少,但速度慢。所以取了折衷的頁級,一次鎖定相鄰的一組記錄。BDB支持頁級鎖

兼容性

共享鎖||讀鎖||S 鎖(share lock)

共享鎖||讀鎖||S 鎖(share lock):其他事務可以讀,但不能寫。允許一個事務去讀一行,阻止其他事務獲得相同數據集的排他鎖。

例如:若事務T對數據對象A加上S鎖,則事務T可以讀A但不能修改A,其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。這保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。

select ... lock in share mode;

排他鎖||寫鎖||X 鎖(exclusive)

其他事務不能讀取,也不能寫。允許獲得排他鎖的事務更新數據,阻止其他事務取得相同數據集的共享讀鎖和排他寫鎖。

例如:若事務T對數據對象A加上X鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的鎖。這保證了其他事務在T釋放A上的鎖之前不能再讀取和修改A。

select ... for update;

鎖模式

一、意向鎖

1、意向共享鎖(IS Lock/intent share lock)

事務想要獲得一張表中某幾行的共享鎖
事務務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的 IS 鎖

2、意向排他鎖||互斥鎖(IX Lock/intent exclusive lock)

事務想要獲得一張表中某幾行的排他鎖
事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的 IX 鎖。

意向鎖的加鎖方式

意向鎖是 InnoDB 自動加的, 不需用戶干預。

意向鎖有什麼用?

主要作用是處理行鎖和表鎖之間的矛盾,能夠顯示“某個事務正在某一行上持有了鎖,或者準備去持有鎖”
當我們需要加一個排他鎖時,需要根據意向鎖去判斷表中有沒有數據行被鎖定

比如事務A要在一個表上加S鎖,如果表中的一行已被事務B加了X鎖,那麼該鎖的申請也應被阻塞。如果表中的數據很多,逐行檢查鎖標誌的開銷將很大,系統的性能將會受到影響。爲了解決這個問題,可以在表級上引入新的鎖類型來表示其所屬行的加鎖情況,這就引出了“意向鎖”的概念。

舉個例子,如果表中記錄1億,事務A把其中有幾條記錄上了行鎖了,這時事務B需要給這個表加表級鎖,如果沒有意向鎖的話,那就要去表中查找這一億條記錄是否上鎖了。如果存在意向鎖,那麼假如事務A在更新一條記錄之前,先加意向鎖,再加X鎖,事務B先檢查該表上是否存在意向鎖,存在的意向鎖是否與自己準備加的鎖衝突,如果有衝突,則等待直到事務A釋放,而無須逐條記錄去檢測。事務B更新表時,其實無須知道到底哪一行被鎖了,它只要知道反正有一行被鎖了就行了。

二、行鎖的算法

1、Record Lock(單行記錄)

單條索引上加鎖,record lock 永遠鎖的是索引,而非數據本身,如果innodb表中沒有索引,那麼會自動創建一個隱藏的聚集索引,鎖住的就是這個聚集索引。當一條sql沒有走任何索引時,那麼將會在每一條聚集索引後面加X鎖,這個類似於表鎖,但原理上和表鎖應該是完全不同的。

  • 記錄鎖的條件
    1、命中單行記錄並且命中的條件字段是唯一索引或者主索引;
update user_info set name=’張三’ where id=1;//這裏的id是唯一索引,使用了Record Lock

Record Lock總是會去鎖住索引記錄,如果InnoDB存儲引擎表在建立的時候沒有設置任何一個索引,那麼這時InnoDB存儲引擎會使用隱式的主鍵來進行鎖定。

2、Gap Lock(間隙鎖)

間隙鎖是封鎖索引記錄中的間隔,或者第一條索引記錄之前的範圍,又或者最後一條索引記錄之後的範圍。

  • 產生間隙的條件
    1、使用普通索引鎖定;
    2、使用多列唯一索引;
    3、使用唯一索引鎖定多行記錄。

我們先理解什麼是間隙,如

CREATE TABLE `test` (
  `id` int(1) NOT NULL AUTO_INCREMENT,
  `name` varchar(8) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `test` VALUES ('1', '小羅');
INSERT INTO `test` VALUES ('5', '小黃');
INSERT INTO `test` VALUES ('7', '小明');
INSERT INTO `test` VALUES ('11', '小紅');

上面數據的間隙就是:

  • (-infinity, 1]
  • (1, 5]
  • (5, 7]
  • (7, 11]
  • (11, +infinity]

在test中使用間隙鎖的話,鎖的就是上面的範圍
基於上面的表,看看下面的例子:

BEGIN;
/* 查詢 id 在 7 - 11 範圍的數據並加記錄鎖 */
SELECT * FROM `test` WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;
/* 延遲30秒執行,防止鎖釋放 */
SELECT SLEEP(30);

產生的間隙鎖範圍是,會鎖住 (5, 7]、(7, 11] 這兩個區間。

BEGIN;
SELECT * FROM `test` WHERE `id` = 3 FOR UPDATE;
SELECT SLEEP(30);

由於3的記錄並不存在,所以也沒有單行記錄可言,也會產生間隙鎖,範圍是(1,3]、(3,5]

BEGIN;
SELECT * FROM `test` WHERE `id` > 5 FOR UPDATE;
SELECT SLEEP(30);

會產生間隙鎖,範圍是(5,+infinity]
理解了間隙,我們看看下面的例子:

mysql> select * from product_copy;
+----+--------+-------+-----+
| id | name   | price | num |
+----+--------+-------+-----+
|  1 | 伊利   |    68 |   1 |
|  2 | 蒙牛   |    88 |   1 |
|  6 | tom    |  2788 |   3 |
| 10 | 優衣庫 |   488 |   4 |
+----+--------+-------+-----+
其中id爲主鍵 num爲普通索引
窗口A:
mysql> select * from product_copy where num=3 for update;
+----+------+-------+-----+
| id | name | price | num |
+----+------+-------+-----+
|  6 | tom  |  2788 |   3 |
+----+------+-------+-----+
1 row in set

窗口B:
mysql> insert into product_copy values(5,'kris',1888,2);
這裏會等待  直到窗口A commit纔會顯示下面結果
Query OK, 1 row affected

但是下面是不需要等待的
mysql> update product_copy set price=price+100 where num=1;
Query OK, 2 rows affected
Rows matched: 2  Changed: 2  Warnings: 0
mysql> insert into product_copy values(5,'kris',1888,5);
Query OK, 1 row affected

通過上面的例子可以看出Gap 鎖的作用是在的範圍是(1,3]U[3,4)。
但是要記住,使用主鍵索引/唯一索引條件的單行記錄不會使用間隙鎖,會使用記錄鎖,但是如果查詢出來的是多行記錄,使用的就是間隙鎖

下面的例子,id是主鍵索引,使用的是記錄鎖

窗口A:
mysql> select * from product_copy where id=6 for update;
+----+------+-------+-----+
| id | name | price | num |
+----+------+-------+-----+
|  6 | tom  |  2788 |   3 |
+----+------+-------+-----+

窗口B:並不會發生等待
mysql> insert into product_copy values(5,'kris',1888,3);
Query OK, 1 row affected

窗口A:
mysql> select * from product_copy where id>6 for update;
+----+------+-------+-----+
| id | name | price | num |
+----+------+-------+-----+
| 10 | 優衣庫 |   488 |   4 |
+----+------+-------+-----+

窗口B:會發生等待
mysql> insert into product_copy values(9,'kris',1888,3);
Query OK, 1 row affected

從上面的例子可以看到,第一條sql是使用了主鍵索引的單行記錄,使用了記錄鎖,第二個sql即使使用了主鍵索引,但是查詢的數據多於一條,使用的間隙鎖,間隙鎖鎖的範圍是(6,+infinity]

3、Next-Key Lock(Record Lock + Gap Lock,臨鍵鎖)

臨鍵鎖,是記錄鎖與間隙鎖的組合,它的封鎖範圍,既鎖住記錄本身還鎖住索引之間的間隙。

注:臨鍵鎖的主要目的,也是爲了避免幻讀(Phantom Read)。如果把事務的隔離級別降級爲RC,臨鍵鎖則也會失效。

MVCC,多版本的併發控制,Multi-Version Concurrency Control。

MVCC的目的就是多版本併發控制,在數據庫中的實現,就是爲了解決讀寫衝突。使用鎖和鎖協議來實現相應的隔離級別來進行併發控制會因爲鎖會造成事務阻塞。而多版本併發控制使得對同一行記錄做讀寫的事務之間不用相互阻塞等待,提高了事務的併發能力,可以認爲MVCC是一種解決讀寫阻塞等待的行級鎖。

MVCC的數據庫表中每一行數據都可能存在多個版本,對數據庫的任何修改的提交都不會直接覆蓋之前的數據,而是產生一個新的版本與老版本共存,通過讀寫數據時讀不同的版本來避免加鎖阻塞

  • 1、MVCC只支持(已提交讀)和(可重複讀)隔離級別。
  • 2、MVCC能解決髒讀、不可重複讀問題,不能解決幻讀問題。
  • 3、MVCC是用來解決讀寫操作之間的阻塞問題。

隱式字段

每行記錄除了我們自定義的字段外,還有數據庫隱式定義的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

  • DB_TRX_ID:數據行版本號:大小爲6byte,記錄最近修改(修改/插入)事務ID,記錄創建這條記錄/最後一次修改該記錄的事務ID
  • DB_ROLL_PTR:刪除版本號:大小爲7byte,記錄回滾指針,指向當前記錄行的undo log信息(指向該數據的前一個版本數據)
  • DB_ROW_ID:行數據隱式id:大小爲6byte,隱含的自增ID(隱藏主鍵),如果數據表沒有主鍵,InnoDB會自動以DB_ROW_ID
    產生一個聚簇索引
ReadView

read view是讀視圖,其實就相當於一種快照,裏面記錄了系統中當前活躍事務的ID以及相關信息,主要用途是用來做可見性判斷,判斷當前事務是否有資格訪問該行數據。read view有多個變量:

  • trx_ids: 它裏面的trx_ids變量存儲了活躍事務列表,也就是Read View開始創建時其他未提交的活躍事務的ID列表。例如事務A在創建read view(快照)時,數據庫中事務B和事務C還沒提交或者回滾結束事務,此時trx_ids就會將事務B和事務C的事務ID記錄下來。

假設當前事務生成了一個ReadView,trx_ids列表裏的事務id爲[60,100]。

1、如果你要訪問的記錄版本的事務id爲50,比當前列表最小的id 60還小,那說明這個事務在ReadView生成之前就提交了,所以對當前活動的事務來說是可訪問的。
2、如果你要訪問的記錄版本的事務id爲70,發現此事務在列表id最大值和最小值之間,那就再判斷一下 70 這個id是否在列表內,如果在那就說明此事務還未提交,所以版本不能被訪問。如果不在那說明事務已經提交,所以版本可以被訪問。
3、如果你要訪問的記錄版本的事務id爲110,那比事務列表最大id100都大,那說明這個版本是在ReadView生成之後才發生的,所以不能被訪問。

Undo log

Undo log中存儲的是老版本數據,當一個事務需要讀取記錄行時,如果當前記錄行不可見,可以通過回滾指針順着undo log鏈找到滿足其可見性條件的記錄行版本。

在InnoDB裏,undo log分爲如下兩類:
①insert undo log : 事務對insert新記錄時產生的undo log, 只在事務回滾時需要, 並且在事務提交後就可以立即丟棄。
②update undo log : 事務對記錄進行delete和update操作時產生的undo log,在事務回滾時需要

實際還有一個刪除flag隱藏字段, 既記錄被更新或刪除並不代表真的刪除,而是刪除flag變了

插入:獲取最新的事務版本號n,保存n到對應行的行版本號

刪除:獲取最新的事務版本號n,保存到對應行的刪除版本號

修改:變爲insert和delete操作的組合,先獲取最新的事務版本號n,然後進行數據行拷貝,插入拷貝的數據,保存n到新插入數據行行版本號的字段中,然後保存n到舊的數據行的刪除版本號字段中

查詢:獲取最新的事務版本號n,查詢行版本號小於或者等於n的行數據,防止讀到其他事務提交的數據

MVCC實現過程原理主要的原理:

參考:https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc

版本記錄都是去版本鏈裏面找的,然後根據不同隔離級別生成的ReadView就會有所不同

  • 例如:在一個讀已提交或者是重複讀的級別事務中

    有一個事務id爲100的事務,修改了name,使得的name等於小明2,但是事務還沒提交。則此時的版本鏈如下:此時另一個事務發起了select 語句要查詢id爲1的記錄,那此時生成的ReadView 列表就是[100]。最新版本100是活躍事務不能訪問,那就得使用版本鏈去找了,首先找100的下個版本name爲小明1的記錄,發現trx_id是60,小於列表中的最小id,所以可以訪問,直接訪問結果爲小明1。那這時候我們把事務id爲100的事務提交了,並且新建了一個事務id爲110也修改id爲1的記錄,name修改爲小明3,並且不提交事務

    這時候之前那個select事務又執行了一次查詢,要查詢id爲1的記錄。這時候會發生兩種情況

  • 1、已提交讀隔離級別
    會重新一個生成一個ReadView,那你的活動事務列表中的值就變了,變成了[110],通過版本鏈查trx_id對比,查到的只能是小明2。
  • 2、可重複讀隔離級別
    ReadView還是第一次select時候生成的ReadView,也就是列表的值還是[100]。所以select的結果是小明1。所以第二次select結果和第一次一樣,所以叫可重複讀!

也就是說已提交讀隔離級別下的事務在每次查詢的開始都會生成一個獨立的ReadView,而可重複讀隔離級別則在第一次讀的時候生成一個ReadView,之後的讀都複用之前的ReadView。

當前讀與快照讀
  • 1、當前讀:
    即加鎖讀,讀取記錄的最新版本,會加鎖保證其他併發事務不能修改當前記錄,直至獲取鎖的事務釋放鎖;
    使用當前讀的操作主要包括:顯式加鎖的讀操作與插入/更新/刪除等寫操作,如下所示:
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
  • 2、快照讀:
    即不加鎖讀,讀取記錄的快照版本而非最新版本,通過MVCC實現;

當前讀
需要特別注意的是在MVCC下的可重複讀在讀操作是防止了幻讀,讀操作下完全就是按照ReadView進行的快照讀。但是對於會對數據修改的操作(update、insert、delete)都是採用當前讀的模式。在執行這幾個操作時會讀取最新的記錄,即使是別的事務提交的數據也可以查詢到。假設要update一條記錄,但是在另一個事務中已經delete掉這條數據並且commit了,如果update就會產生衝突,所以在update的時候需要知道最新的數據。

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