MySQL的鎖機制 - 記錄鎖、間隙鎖、臨鍵鎖 - 咖啡屋小羅的文章 - 知乎 https://zhuanlan.zhihu.com/p/48269420

記錄鎖(Record Locks)

記錄鎖是 封鎖記錄,記錄鎖也叫行鎖,例如:

SELECT * FROM test WHERE id=1 FOR UPDATE; 它會在 id=1 的記錄上加上記錄鎖,以阻止其他事務插入,更新,刪除 id=1 這一行。

間隙鎖(Gap Locks)(重點)

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

產生間隙鎖的條件(RR事務隔離級別下;):

  • 使用普通索引鎖定;
  • 使用多列唯一索引;
  • 使用唯一索引鎖定多行記錄。

以上情況,都會產生間隙鎖,下面是小編看了官方文檔理解的:

對於使用唯一索引來搜索並給某一行記錄加鎖的語句,不會產生間隙鎖。(這不包括搜索條件僅包括多列唯一索引的一些列的情況;在這種情況下,會產生間隙鎖。)例如,如果id列具有唯一索引,則下面的語句僅對具有id值100的行使用記錄鎖,並不會產生間隙鎖:

SELECT * FROM child WHERE id = 100 FOR UPDATE;

這條語句,就只會產生記錄鎖,不會產生間隙鎖。

打開間隙鎖設置,首先查看 innodb_locks_unsafe_for_binlog 是否禁用:

show variables like 'innodb_locks_unsafe_for_binlog';

innodb_locks_unsafe_for_binlog:默認值爲OFF,即啓用間隙鎖。因爲此參數是隻讀模式,如果想要禁用間隙鎖,需要修改 my.cnf(windows是my.ini) 重新啓動纔行。

# 在 my.cnf 裏面的[mysqld]添加
[mysqld]
innodb_locks_unsafe_for_binlog = 1

唯一索引的間隙鎖

環境:MySQL,InnoDB,默認的隔離級別(RR)

數據表:

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', '小紅');

在進行測試之前,我們先來看看test表中存在的隱藏間隙:

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

只使用記錄鎖,不會產生間隙鎖

我們現在進行以下幾個事務的測試:

/* 開啓事務1 */
BEGIN;
/* 查詢 id = 5 的數據並加記錄鎖 */
SELECT * FROM `test` WHERE `id` = 5 FOR UPDATE;
/* 延遲30秒執行,防止鎖釋放 */
SELECT SLEEP(30);

注意:以下的語句不是放在一個事務中執行,而是分開多次執行,每次事務中只有一條添加語句

/* 事務2插入一條 name = '小張' 的數據 */
INSERT INTO `test` (`id`, `name`) VALUES (4, '小張'); # 正常執行

/* 事務3插入一條 name = '小張' 的數據 */
INSERT INTO `test` (`id`, `name`) VALUES (8, '小東'); # 正常執行

/* 提交事務1,釋放事務1的鎖 */
COMMIT;

上訴的案例,由於主鍵是唯一索引,而且是隻使用一個索引查詢,並且只鎖定一條記錄,所以以上的例子,只會對 id = 5 的數據加上記錄鎖,而不會產生間隙鎖。

產生間隙鎖

我們繼續在 id 唯一索引列上做以下的測試:

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

注意:以下的語句不是放在一個事務中執行,而是分開多次執行,每次事務中只有一條添加語句

/* 事務2插入一條 id = 3,name = '小張1' 的數據 */
INSERT INTO `test` (`id`, `name`) VALUES (3, '小張1'); # 正常執行

/* 事務3插入一條 id = 4,name = '小白' 的數據 */
INSERT INTO `test` (`id`, `name`) VALUES (4, '小白'); # 正常執行

/* 事務4插入一條 id = 6,name = '小東' 的數據 */
INSERT INTO `test` (`id`, `name`) VALUES (6, '小東'); # 阻塞

/* 事務5插入一條 id = 8, name = '大羅' 的數據 */
INSERT INTO `test` (`id`, `name`) VALUES (8, '大羅'); # 阻塞

/* 事務6插入一條 id = 9, name = '大東' 的數據 */
INSERT INTO `test` (`id`, `name`) VALUES (9, '大東'); # 阻塞

/* 事務7插入一條 id = 11, name = '李西' 的數據 */
INSERT INTO `test` (`id`, `name`) VALUES (11, '李西'); # 阻塞

/* 事務8插入一條 id = 12, name = '張三' 的數據 */
INSERT INTO `test` (`id`, `name`) VALUES (12, '張三'); # 正常執行

/* 提交事務1,釋放事務1的鎖 */
COMMIT;

從上面我們可以看到,(5, 7]、(7, 11] 這兩個區間,都不可插入數據,其它區間,都可以正常插入數據。所以我們可以得出結論:當我們給 (5, 7] 這個區間加鎖的時候,會鎖住 (5, 7]、(7, 11] 這兩個區間。

我們再來測試如果我們鎖住不存在的數據時,會怎樣:

/* 開啓事務1 / BEGIN; / 查詢 id = 3 這一條不存在的數據並加記錄鎖 / SELECT * FROM test WHERE id = 3 FOR UPDATE; / 延遲30秒執行,防止鎖釋放 */ SELECT SLEEP(30);

注意:以下的語句不是放在一個事務中執行,而是分開多次執行,每次事務中只有一條添加語句

/* 事務2插入一條 id = 3,name = '小張1' 的數據 */ INSERT INTO test (id, name) VALUES (2, '小張1'); # 阻塞

/* 事務3插入一條 id = 4,name = '小白' 的數據 */ INSERT INTO test (id, name) VALUES (4, '小白'); # 阻塞

/* 事務4插入一條 id = 6,name = '小東' 的數據 */ INSERT INTO test (id, name) VALUES (6, '小東'); # 正常執行

/* 事務5插入一條 id = 8, name = '大羅' 的數據 */ INSERT INTO test (id, name) VALUES (8, '大羅'); # 正常執行

/* 提交事務1,釋放事務1的鎖 */ COMMIT; 我們可以看出,指定查詢某一條記錄時,如果這條記錄不存在,會產生間隙鎖。

結論

對於指定查詢某一條記錄的加鎖語句,如果該記錄不存在,會產生記錄鎖和間隙鎖,如果記錄存在,則只會產生記錄鎖,如:WHERE id = 5 FOR UPDATE;

對於查找某一範圍內的查詢語句,會產生間隙鎖,如:WHERE id BETWEEN 5 AND 7 FOR UPDATE;

普通索引的間隙鎖 數據準備

創建 test1 表:

注意:number 不是唯一值

CREATE TABLE `test1` (
  `id` int(1) NOT NULL AUTO_INCREMENT,
  `number` int(1) NOT NULL COMMENT '數字',
  PRIMARY KEY (`id`),
  KEY `number` (`number`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

在這張表上,我們有 id number 這兩個字段,id 是我們的主鍵,我們在 number 上,建立了一個普通索引,爲了方便我們後面的測試。現在我們要先加一些數據:

INSERT INTO `test1` VALUES (1, 1);
INSERT INTO `test1` VALUES (5, 3);
INSERT INTO `test1` VALUES (7, 8);
INSERT INTO `test1` VALUES (11, 12);

在進行測試之前,我們先來看看test1表中 number 索引存在的隱藏間隙:

(-infinity, 1]
(1, 3]
(3, 8]
(8, 12]
(12, +infinity]

案例說明:

我們執行以下的事務(事務1最後提交),分別執行下面的語句:

/* 開啓事務1 */
BEGIN;
/* 查詢 number = 5 的數據並加記錄鎖 */
SELECT * FROM `test1` WHERE `number` = 3 FOR UPDATE;
/* 延遲30秒執行,防止鎖釋放 */
SELECT SLEEP(30);

注意:以下的語句不是放在一個事務中執行,而是分開多次執行,每次事務中只有一條添加語句

/* 事務2插入一條 number = 0 的數據 */
INSERT INTO `test1` (`number`) VALUES (0); # 正常執行

/* 事務3插入一條 number = 1 的數據 */
INSERT INTO `test1` (`number`) VALUES (1); # 被阻塞

/* 事務4插入一條 number = 2 的數據 */
INSERT INTO `test1` (`number`) VALUES (2); # 被阻塞

/* 事務5插入一條 number = 4 的數據 */
INSERT INTO `test1` (`number`) VALUES (4); # 被阻塞

/* 事務6插入一條 number = 8 的數據 */
INSERT INTO `test1` (`number`) VALUES (8); # 正常執行

/* 事務7插入一條 number = 9 的數據 */
INSERT INTO `test1` (`number`) VALUES (9); # 正常執行

/* 事務8插入一條 number = 10 的數據 */
INSERT INTO `test1` (`number`) VALUES (10); # 正常執行

/* 提交事務1 */
COMMIT;

我們會發現有些語句可以正常執行,有些語句被阻塞了。我們再來看看我們表中的數據:

執行之後的數據 這裏可以看到,number (1 - 8) 的間隙中,插入語句都被阻塞了,而不在這個範圍內的語句,正常執行,這就是因爲有間隙鎖的原因。我們再進行以下的測試,方便我們更好的理解間隙鎖的區域(我們要將數據還原成原來的那樣):

/* 開啓事務1 */
BEGIN;
/* 查詢 number = 5 的數據並加記錄鎖 */
SELECT * FROM `test1` WHERE `number` = 3 FOR UPDATE;
/* 延遲30秒執行,防止鎖釋放 */
SELECT SLEEP(30);

/* 事務1插入一條 id = 2, number = 1 的數據 */
INSERT INTO `test1` (`id`, `number`) VALUES (2, 1); # 阻塞

/* 事務2插入一條 id = 3, number = 2 的數據 */
INSERT INTO `test1` (`id`, `number`) VALUES (3, 2); # 阻塞

/* 事務3插入一條 id = 6, number = 8 的數據 */
INSERT INTO `test1` (`id`, `number`) VALUES (6, 8); # 阻塞

/* 事務4插入一條 id = 8, number = 8 的數據 */
INSERT INTO `test1` (`id`, `number`) VALUES (8, 8); # 正常執行

/* 事務5插入一條 id = 9, number = 9 的數據 */
INSERT INTO `test1` (`id`, `number`) VALUES (9, 9); # 正常執行

/* 事務6插入一條 id = 10, number = 12 的數據 */
INSERT INTO `test1` (`id`, `number`) VALUES (10, 12); # 正常執行

/* 事務7修改 id = 11, number = 12 的數據 */
UPDATE `test1` SET `number` = 5 WHERE `id` = 11 AND `number` = 12; # 阻塞

/* 提交事務1 */
COMMIT;

我們來看看結果:

執行後的數據 這裏有一個奇怪的現象:

  • 事務3添加 id = 6,number = 8 的數據,給阻塞了;
  • 事務4添加 id = 8,number = 8 的數據,正常執行了。
  • 事務7將 id = 11,number = 12 的數據修改爲 id = 11, number = 5的操作,給阻塞了; 這是爲什麼呢?我們來看看下邊的圖,大家就明白了。

隱藏的間隙鎖圖,從圖中可以看出,當 number 相同時,會根據主鍵 id 來排序,所以:

  • 事務3添加的 id = 6,number = 8,這條數據是在 (3, 8) 的區間裏邊,所以會被阻塞;
  • 事務4添加的 id = 8,number = 8,這條數據則是在(8, 12)區間裏邊,所以不會被阻塞;
  • 事務7的修改語句相當於在 (3, 8) 的區間裏邊插入一條數據,所以也被阻塞了。

結論

  • 在普通索引列上,不管是何種查詢,只要加鎖,都會產生間隙鎖,這跟唯一索引不一樣;
  • 在普通索引跟唯一索引中,數據間隙的分析,數據行是優先根據普通索引排序,再根據唯一索引排序。

臨鍵鎖(Next-key Locks)

臨鍵鎖,是記錄鎖與間隙鎖的組合,它的封鎖範圍,既包含索引記錄,又包含索引區間。

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

要點

  • 記錄鎖、間隙鎖、臨鍵鎖,都屬於排它鎖;
  • 記錄鎖就是鎖住一行記錄;
  • 間隙鎖只有在事務隔離級別 RR 中才會產生;
  • 唯一索引只有鎖住多條記錄或者一條不存在的記錄的時候,纔會產生間隙鎖,指定給某條存在的記錄加鎖的時候,只會加記錄鎖,不會產生間隙鎖;
  • 普通索引不管是鎖住單條,還是多條記錄,都會產生間隙鎖;
  • 間隙鎖會封鎖該條記錄相鄰兩個鍵之間的空白區域,防止其它事務在這個區域內插入、修改、刪除數據,這是爲了防止出現 幻讀 現象;
  • 普通索引的間隙,優先以普通索引排序,然後再根據主鍵索引排序(多普通索引情況還未研究);
  • 事務級別是RC(讀已提交)級別的話,間隙鎖將會失效。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章