詳細介紹MySQL/MariaDB的鎖


官方手冊:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-transaction-model.html

1.事務提交的方式

在MariaDB/MySQL中有3種事務提交的方式。

1.顯式開啟和提交。

使用begin或者start transaction來顯式開啟一個事務,顯式開啟的事務必須使用commit或者rollback顯式提交或回滾。幾種特殊的情況除外:行版本隔離級別下的更新沖突和死鎖會自動回滾。

在存儲過程中開啟事務時必須使用start transaction,因為begin會被存儲過程解析為begin...end結構塊。

另外,MariaDB/MySQL中的DDL語句會自動提交前面所有的事務(包括顯示開啟的事務),而在SQL Server中DDL語句還是需要顯式提交的,也就是說在SQL Server中DDL語句也是可以回滾的。

2.自動提交。(MySQL默認的提交方式)

不需要顯式begin或者start transaction來顯式開啟事務,也不需要顯式提交或回滾事務,每次執行DML和DDL語句都會在執行語句前自動開啟一個事務,執行語句結束后自動提交或回滾事務。

3.隱式提交事務

隱式提交事務是指執行某些語句會自動提交事務,包括已經顯式開啟的事務。

會隱式提交事務的語句主要有:

(1).DDL語句(其中有truncate table)。

(2).隱式修改mysql數據庫架構的操作:create user,drop user,grant,rename user,revoke,set password。

(3).管理語句:analyze table、cache index、check table、load index into cache、optimize table、repair table。

通過設置 auto_commit 變量值為1或0來設置是否自動提交,為1表示自動提交,0表示關閉自動提交,即必須顯式提交。但是不管設置為0還是1,顯式開啟的事務必須顯式提交,而且隱式提交的事務不受任何人為控制。

2.MariaDB/MySQL中的鎖

鎖和事務的實現是存儲引擎內的組件管理的,而MariaDB/MySQL是插件式的存儲引擎實現方式,所以不同的存儲引擎可以支持不同級別的鎖和事務。

2.1 不同存儲引擎支持的鎖級別

MariaDB/MySQL相比其他數據產品來說,支持的鎖比較簡單。

1.MyISAM、Aria(MariaDB中對myisam的改進版本)和memory存儲引擎只支持表級別的鎖。

2.innodb支持行級別的鎖和表級別的鎖,默認情況下在允許使用行級別鎖的時候都會使用行級別的鎖。

3.DBD存儲引擎支持頁級別和表級別的鎖。

2.2 鎖類型

在MariaDB/MySQL中只有簡單的幾種鎖類型:

1.共享鎖(S):即讀鎖,不涉及修改數據,在檢索數據時才申請的鎖。

2.獨占鎖(X):增、刪、改等涉及修改操作的時候,都會申請獨占鎖。

以上是支持表鎖的存儲引擎都會有的鎖類型。以下兩種是支持行鎖或頁鎖才會有的鎖類型,也就是說myisam沒有下面的鎖,而innodb有。

3.意向共享鎖(IS):獲取低級別共享鎖的同時,在高級別上也獲取特殊的共享鎖,這種特殊的共享鎖是意向共享鎖。

4.意向獨占鎖(IX):獲取低級別獨占鎖的同時,在高級別上也獲取特殊的獨占鎖,這種特殊的獨占鎖是意向獨占鎖。

低級別鎖表示的是行鎖或頁鎖,意向鎖可能是多條記錄組成的范圍鎖,也可能直接就是表意向鎖。

2.3 鎖兼容性

如下表:

獨占鎖和所有的鎖都沖突,意向共享鎖和共享鎖兼容(這是肯定的),還和意向獨占鎖兼容。所以加了意向共享鎖的時候,可以修改行級非共享鎖的記錄。同理,加了意向獨占鎖的時候,可以檢索這些加了獨占鎖的記錄。

3.MyISAM的表級鎖(lock tables和unlock語句)

MariaDB/MySQL中myisam和innodb都支持表級鎖。表級鎖分為兩種:讀鎖(read lock)和寫鎖(write lock)。本節所述均為myisam支持的,同樣innodb也一樣支持。

可以通過語句來實現表級鎖的鎖定和解鎖,這些語句的操作環境是當前客戶端會話(即作用范圍是會話)。鎖表的時候可以一次性鎖定多張表,並使用不同的鎖,而解鎖的時候只能一次性解鎖當前客戶端會話的所有表。

LOCK TABLES tbl_name [[AS] alias] lock_type [, tbl_name [[AS] alias] lock_type] ...
lock_type:
    READ [LOCAL]
  | [LOW_PRIORITY] WRITE
 
UNLOCK TABLES

lock tables命令可以鎖表或鎖視圖,鎖視圖的時候會自動將視圖內的基表加上對應類型的鎖。由於MariaDB/MySQL中觸發器是基於表的,所以lock tables鎖定表的時候,觸發器內使用的表也都會被鎖定。

例如:table1上有一個如下觸發器:

CREATE TRIGGER trigger1 AFTER INSERT ON table1 FOR EACH ROW
BEGIN
  INSERT INTO table2 VALUES (1);
  UPDATE table3 SET writes = writes+1
    WHERE id = NEW.id AND EXISTS (SELECT id FROM table4);
END;

如果為table1加上寫鎖,則table2、table3都會加上寫鎖,而table4會加上讀鎖。

lock tables命令會隱式釋放當前客戶端會話中之前的所有鎖。

現在創建3張表作為測試表。

DROP TABLE IF EXISTS t1,t2,t3;
CREATE TABLE t1(a INT,b CHAR(5))ENGINE=MYISAM;
CREATE TABLE t2(a INT,b CHAR(5))ENGINE=MYISAM;
CREATE TABLE t3(a INT,b CHAR(5))ENGINE=MYISAM;
INSERT INTO t1 VALUES(1,'a');
INSERT INTO t2 VALUES(1,'a');
INSERT INTO t3 VALUES(1,'a');

給t1加上讀鎖。

LOCK TABLES t1 READ;

此時當前會話將無法操作t1以外的任何表,連查詢也不允許,因為只有t1表加了鎖。而其他會話則可以進行查詢,但不能進行更新。

當再次使用lock tables命令的時候,會先釋放當前會話之前所有的鎖,再對lock tables命令中的表申請鎖。

例如,上面會話1鎖了表t1,此時無法操作t2表。現在對t2表lock table。

lock tables t2 read;

此時就可以操作t2表而不能操作t1表了,因為對t1表的鎖已經釋放了。

使用lock tables給表加讀鎖的時候,還有一個選項local,該選項表示對當前現有的記錄加上鎖,不影響其他會話的插入記錄語句。但是否真的能插入,由變量concurrent_insert決定,該變量默認值為auto。關於並發插入,見我翻譯的官方手冊:https://mariadb.com/kb/zh-cn/concurrent-inserts/

如果設置為2,那么對myisam表的並發插入有一定提升。

現在測試默認的情況,即 concurrent_insert=auto 的情況。

insert into t1 values(2,'c'),(3,'d'),(4,'e'),(5,'f');

show variables like "%concurrent_insert%";
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| concurrent_insert | AUTO  |
+-------------------+-------+
1 row in set

lock tables t1 read local;

在另一個會話中插入一條記錄,這是允許的操作。當然,在鎖表的會話中肯定是不能插入的。

insert into t1 values(8,'h');

解鎖,並刪除中間的兩條記錄,形成空洞。然后再鎖定表。

mysql> unlock tables;
mysql> delete from t1 where a=3 or a=4;
mysql> lock tables t1 read local;

在其他會話中插入記錄。會發現被阻塞。當表解鎖后立即成功插入。

insert into t1 values(3,'h'),(9,'i'),(8,'g');

將concurrent_insert設置為2,即always,此時不管是否有空洞都允許向myisam表尾部插入。

delete from t1 where a=3 or a=8 or a=9;
set @@global.concurrent_insert=2;
lock tables t1 read local;
insert into t1 values(3,'d'),(8,'g'),(9,'i');

此時發現能夠正常插入,且查詢t1表發現,這些記錄都插入在表的尾部。

默認情況下,使用表級鎖的存儲引擎中(所以innodb不支持),寫鎖的優先級高於讀鎖。這意味着,當表上已經有一個寫鎖的時候,后續的寫操作、讀操作都會隊列化,且隊列中的寫操作總是在讀操作之前執行,即使寫操作比讀操作后到達MySQL/MariaDB服務器。可以改變這種優先級。詳細內容見:http://www.cnblogs.com/f-ck-need-u/p/8907252.html

4.innodb中的鎖

innodb支持行級鎖,也是在允許的情況下默認申請的鎖。

SQL Server中的鎖是一種稀有資源,且會在需要的時候鎖升級,所以鎖越多性能越差。而MariaDB/MySQL中的鎖不是稀有資源,不會進行鎖升級,因此鎖的多少不會影響性能,1個鎖和1000000個鎖性能是一樣的(不考慮鎖占用的內存),鎖的多少只會影響並發性。

4.1 查看鎖信息的幾種方法

現在人為造成一個鎖等待。

會話1執行:

begin;
update tt set b='h' where a=1;

會話2執行:

begin;
update tt set b= 'x' where a=1;

此時會話2被阻塞,進入鎖等待狀態。

要查看鎖信息。有幾種方法:

1.通過show engine innodb status來查看,其中的transactions片段可以看到事務,其中包括鎖等待。

以下是沒有激活任何事務的信息:

mysql> show engine innodb status;
------------
TRANSACTIONS
------------
Trx id counter 2856
Purge done for trx's n:o < 2856 undo n:o < 0 state: running
History list length 25
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421383739060216, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421383739059200, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421383739057168, not started
0 lock struct(s), heap size 1136, 0 row lock(s)

三個"---TRANSACTION"表示當前開啟了3個mysql會話,但這3個會話都沒有任何事務。

以下是某會話開啟一個事務,但沒有任何鎖等待的事務信息:

mysql> show engine innodb status;
------------
TRANSACTIONS
------------
Trx id counter 2857
Purge done for trx's n:o < 2856 undo n:o < 0 state: running
History list length 25
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421383739060216, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421383739057168, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 2856, ACTIVE 10 sec
1 lock struct(s), heap size 1136, 0 row lock(s), undo log entries 1
MySQL thread id 39, OS thread handle 139909209945856, query id 1814112 localhost root Reset for next command

不難看出,這個事務是一個需要寫日志的DML事務。

以下是有鎖等待的事務信息:

mysql> show engine innodb status;
------------
TRANSACTIONS
------------
Trx id counter 14915
Purge done for trx's n:o < 14912 undo n:o < 0 state: running but idle
History list length 896
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 14909, not started
MySQL thread id 36, OS thread handle 0x7f5d57e4b700, query id 961 localhost root init
show engine innodb status
---TRANSACTION 14914, ACTIVE 465 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1184, 1 row lock(s)
MySQL thread id 34, OS thread handle 0x7f5d57e8c700, query id 959 localhost root updating
update tt set b= 'x' where a=1
------- TRX HAS BEEN WAITING 13 SEC FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 184 page no 3 n bits 80 index `GEN_CLUST_INDEX` of table `test`.`tt` trx id 14914 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 6; hex 000000000601; asc       ;;
 1: len 6; hex 000000003a41; asc     :A;;
 2: len 7; hex 2f000001580feb; asc /   X  ;;
 3: len 4; hex 80000001; asc     ;;
 4: len 5; hex 6820202020; asc h    ;;

------------------
---TRANSACTION 14913, ACTIVE 490 sec
2 lock struct(s), heap size 360, 6 row lock(s), undo log entries 1
MySQL thread id 1, OS thread handle 0x7f5d57f4f700, query id 900 localhost root

從上面的結果可以看到鎖等待的信息。

"TRX HAS BEEN WAITING 13 SEC FOR THIS LOCK TO BE GRANTED"表示該事務申請鎖已經等待了13秒。

"RECORD LOCKS space id 184 page no 3 n bits 80 index `GEN_CLUST_INDEX` of table `test`.`tt` trx id 14914 lock_mode X waiting"表示test.tt表上的記錄要申請的行鎖(recode lock)是獨占鎖並且正在waiting,並且標明了該行記錄所在表數據文件中的物理位置:表空間id為184,頁碼為3。

關於這些信息的詳細解釋,后文會逐漸說明。

2.使用show processlist查看。

show full processlist;

從上面的結果可以看出,update語句一直處於updating狀態。所以,該方法查出來的並不一定是鎖等待,有可能是更新的記錄太多或者其他問題,總之這里看出來的是該語句還沒有執行完成。

3.查看information_schema中的數據字典

在information_schema架構下,有3個表記錄了事務和鎖相關的信息。分別是INNODB_TRX,INNODB_LOCKS,INNODB_LOCK_WAITS

這三個表可能相對復雜,以下分別說明這3張表的各列。

根據上面實驗過程中的鎖查看該表的部分結果如下:

mysql> select * from information_schema.INNODB_TRX\G
*************************** 1. row ***************************
                    trx_id: 14914
                 trx_state: LOCK WAIT
               trx_started: 2017-03-30 06:07:51
     trx_requested_lock_id: 14914:184:3:2
          trx_wait_started: 2017-03-30 06:39:25
                trx_weight: 2
       trx_mysql_thread_id: 34
                 trx_query: update tt set b= 'x' where a=1
*************************** 2. row ***************************
                    trx_id: 14913
                 trx_state: RUNNING
               trx_started: 2017-03-30 06:07:26
     trx_requested_lock_id: NULL
          trx_wait_started: NULL
                trx_weight: 3
       trx_mysql_thread_id: 1
                 trx_query: NULL

從結果中可以看出id為14914的事務正處於鎖等待狀態,該事務中要申請鎖的語句是update語句,也就是說是因為該語句而導致的鎖等待。

從innodb_trx表中只能查看到事務的信息,而不能看到鎖相關的信息。要看鎖的信息,需要查看表innodb_locks。

mysql> select * from information_schema.INNODB_LOCKS\G
*************************** 1. row ***************************
    lock_id: 14914:184:3:2
lock_trx_id: 14914
  lock_mode: X
  lock_type: RECORD
 lock_table: `test`.`tt`
 lock_index: GEN_CLUST_INDEX
 lock_space: 184
  lock_page: 3
   lock_rec: 2
  lock_data: 0x000000000601
*************************** 2. row ***************************
    lock_id: 14913:184:3:2
lock_trx_id: 14913
  lock_mode: X
  lock_type: RECORD
 lock_table: `test`.`tt`
 lock_index: GEN_CLUST_INDEX
 lock_space: 184
  lock_page: 3
   lock_rec: 2
  lock_data: 0x000000000601
2 rows in set (0.00 sec)

從上面的結果中看出,鎖所在的事務ID為14914,並且鎖模式為獨占鎖,類型為record即行鎖,申請鎖的表為tt表,而且鎖定的頁數為3頁,鎖定的行有2行,鎖定行的主鍵值為0x000000000601。也許會奇怪,在前面實驗過程中根本就沒有建立主鍵,這里為什么會有主鍵值,這是因為MySQL在加鎖的時候判斷是否有索引,沒有索引的時候會自動隱式的添加索引(聚集索引),從上面鎖的索引為"GEN_CLUST_INDEX"可以看出。

所以我們可以知道,MariaDB/MySQL中的行鎖是通過鍵鎖(Key)來實現的(在SQL Server中有堆表的概念,SQL Server對於沒有索引的表,其行鎖通過rid鎖來實現)。

並且從上面的兩段結果也可以看到,它們的申請鎖資源所處位置是相同的,正因為位置相同,所以才有了鎖等待。

現在在會話1上創建索引,然后人為造成鎖等待再來查看innodb_locks表。

在會話1和會話2執行:

rollback;

在會話1執行:

create index idx_tt on tt(a);
begin;
update tt set b='h' where a=1;

在會話2執行:

begin;
update tt set b='x' where a=1;

查看innodb_locks表。

mysql> select * from information_schema.INNODB_LOCKS\G
*************************** 1. row ***************************
    lock_id: 14925:184:4:2
lock_trx_id: 14925
  lock_mode: X
  lock_type: RECORD
 lock_table: `test`.`tt`
 lock_index: ind_tt
 lock_space: 184
  lock_page: 4
   lock_rec: 2
  lock_data: 1, 0x000000000601
*************************** 2. row ***************************
    lock_id: 14924:184:4:2
lock_trx_id: 14924
  lock_mode: X
  lock_type: RECORD
 lock_table: `test`.`tt`
 lock_index: ind_tt
 lock_space: 184
  lock_page: 4
   lock_rec: 2
  lock_data: 1, 0x000000000601
2 rows in set (0.00 sec)

此處發現,鎖的索引類型為ind_tt,而鎖住行的主鍵值已經變為1個1了。

查出了鎖的信息后,就可以人為的判斷出鎖等待信息。但是當事務比較大的時候,鎖的信息非常繁雜,這時候通過上面的兩張表無法輕易判斷相關鎖信息。由此要借助第三張表 innodb_lock_waits ,該表只有4列,且意義直觀明了。

還是上面試驗過程中造成的鎖等待,查看那innodb_lock_waits表結果如下:

mysql> select * from information_schema.INNODB_LOCK_WAITS\G
*************************** 1. row ***************************
requesting_trx_id: 14914
requested_lock_id: 14914:184:3:2
  blocking_trx_id: 14913
 blocking_lock_id: 14913:184:3:2
1 row in set (0.00 sec)

可以看到,申請鎖的事務ID為14914,阻塞在前方的事務ID為14913。

有了這3張表,還可以將它們聯接起來更直觀的顯示想要的結果。如下:

SELECT
    r.trx_id AS waiting_trx_id,
    r.trx_mysql_thread_id AS waiting_thread,
    r.trx_query AS waiting_query,
    b.trx_id AS blocking_trx_id,
    b.trx_mysql_thread_id AS blocking_thread,
    b.trx_query AS blocking_query
FROM
    information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id\G
*************************** 1. row ***************************
 waiting_trx_id: 14925
 waiting_thread: 34
  waiting_query: update tt set b='x' where a=1
blocking_trx_id: 14924
blocking_thread: 1
 blocking_query: NULL

現在可以直觀的看到14925事務被阻,語句為update,阻塞它的事務為14924。

還可以從以下聯接語句中查看鎖和事務的相關信息。

SELECT
    trx_id,
    trx_state,
    lock_id,
    lock_mode,
    lock_type,
    lock_table,
    trx_mysql_thread_id,
    trx_query
FROM
    information_schema.innodb_trx t
JOIN information_schema.innodb_locks l ON l.lock_trx_id = t.trx_id;

4.2 innodb表的外鍵和鎖

在innodb表中,創建外鍵的時候若外鍵列上沒有索引,則會在創建過程中自動在外鍵列上隱式地創建索引。

存在這樣一種情況,當向子表中插入數據的時候,會向父表查詢該表中是否存在對應的值以判斷將要插入的記錄是否滿足外鍵約束,也就是說會對父表中對應的記錄加上依賴性的共享鎖,並在表上加意向共享鎖。如果此時父表上對應的記錄正好有獨占鎖,那么插入就會失敗。同理,從子表中刪除或更新記錄也是一樣的。

現在創建父表parent和子表child,並不要在外鍵列(pid)上顯式創建索引。

create table parent(pid int primary key);
create table child(cid int primary key,pid int,foreign key(pid) references parent(pid));
show create table child\G
*************************** 1. row ***************************
       Table: child
Create Table: CREATE TABLE `child` (
  `cid` int(11) NOT NULL,
  `pid` int(11) DEFAULT NULL,
  PRIMARY KEY (`cid`),
  KEY `pid` (`pid`),
  CONSTRAINT `child_ibfk_1` FOREIGN KEY (`pid`) REFERENCES `parent` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

從show的結果中可以發現,已經自動添加了索引列pid。

插入一些測試記錄。

insert into parent values(1),(2),(3);

在會話1中執行:

begin;
delete from parent where pid=3;

在會話2中執行:

begin;
insert into child select 3,3;

這時會發現會話2被阻塞了。通過innodb_trx和innodb_locks表的聯合,得到如下結果:

SELECT
    trx_id,
    trx_state,
    lock_id,
    lock_mode,
    lock_type,
    lock_table,
    trx_mysql_thread_id,
    trx_query
FROM
    information_schema.innodb_trx t
JOIN information_schema.innodb_locks l ON l.lock_trx_id = t.trx_id\G
*************************** 1. row ***************************
             trx_id: 14951
          trx_state: LOCK WAIT
            lock_id: 14951:185:3:4
          lock_mode: S
          lock_type: RECORD
         lock_table: `test`.`parent`
trx_mysql_thread_id: 34
          trx_query: insert into child select 3,3
*************************** 2. row ***************************
             trx_id: 14946
          trx_state: RUNNING
            lock_id: 14946:185:3:4
          lock_mode: X
          lock_type: RECORD
         lock_table: `test`.`parent`
trx_mysql_thread_id: 1
          trx_query: NULL

不難看出,insert語句想要在父表parent上的資源"14951:185:3:4"加共享鎖,但是此時父表上該資源已經有了獨占鎖,所以被阻塞了。

並且也可以判斷出,通過外鍵讀取父表時的模式是lock in share mode,而不是基於快照的行版本讀(什么是lock in share mode和行版本快照讀見事務隔離級別內容),假如是基於行版本的快照讀,那么就可以查出存在pid=3的記錄而導致子表插入成功,這樣也可能導致父表和子表不滿足外鍵約束。

4.3 innodb鎖算法

innodb支持行級鎖,但是它還支持范圍鎖。即對范圍內的行記錄加行鎖。

有三種鎖算法:

  • 1.record lock:即行鎖
  • 2.gap lock:范圍鎖,但是不鎖定行記錄本身
  • 3.next-key lock:范圍鎖加行鎖,即范圍鎖並鎖定記錄本身,gap lock + record lock。

record lock是行鎖,但是它的行鎖鎖定的是key,即基於唯一性索引鍵列來鎖定(SQL Server還有基於堆表的rid類型行鎖)。如果沒有唯一性索引鍵列,則會自動在隱式列上創建索引並完成鎖定。

next-key lock是行鎖和范圍鎖的結合,innodb對行的鎖申請默認都是這種算法。如果有索引,則只鎖定指定范圍內的索引鍵值,如果沒有索引,則自動創建索引並對整個表進行范圍鎖定。之所以鎖定了表還稱為范圍鎖定,是因為它實際上鎖的不是表,而是把所有可能的區間都鎖定了,從主鍵值的負無窮到正無窮的所有區間都鎖定,等價於鎖定了表。

以下示例過程將演示范圍鎖的情況。

1.有索引的情況

首先創建一個有索引的表t。然后插入幾個被分隔的記錄。

create table t(id int);
create unique index idx_t on t(id);
insert into t values(1),(2),(3),(4),(7),(8),(12),(15);

在會話1執行:無需知道lock in share mode是什么意思,只需知道它的作用是在讀取的時候加上共享鎖並且不釋放,具體內容在事務章節中會說明。

begin;
select * from t where id<5 lock in share mode;

在會話2執行:

insert into t values(9);
insert into t values(6);

這時發現第一條插入語句是正常插入的,而第二條語句被阻塞。 show engine innodb status 看結果。

mysql> show engine innodb status;
------------
TRANSACTIONS
------------
Trx id counter 14992
Purge done for trx's n:o < 14987 undo n:o < 0 state: running but idle
History list length 914
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 0, not started
MySQL thread id 50, OS thread handle 0x7f5d57e0a700, query id 1495 localhost root init
show engine innodb status
---TRANSACTION 14991, ACTIVE 17 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 360, 1 row lock(s), undo log entries 1
MySQL thread id 49, OS thread handle 0x7f5d57d88700, query id 1491 localhost root update
insert into t values(6)
------- TRX HAS BEEN WAITING 17 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 187 page no 4 n bits 80 index `idx_t` of table `test`.`t` trx id 14991 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 80000007; asc     ;;
 1: len 6; hex 00000000060c; asc       ;;

------------------
---TRANSACTION 14989, ACTIVE 32 sec
2 lock struct(s), heap size 360, 5 row lock(s)
MySQL thread id 43, OS thread handle 0x7f5d57f0e700, query id 1489 localhost root

其中"locks gap"就表示阻塞insert語句的鎖是gap鎖,即范圍鎖。鎖定的范圍包括(-∞,4],(4,7](鎖到操作行的下一個key,此處插入id=6,由於存在id=7的key,所以鎖到7為止,這就是next-key的意思)。當測試插入或修改-1,0,5,6等小於7的值都會被阻塞,而插入或修改大於7的值就不會被阻塞。

如何判斷鎖定的范圍大小?可以通過下面的查詢語句:

mysql> select * from information_schema.INNODB_LOCKS\G
*************************** 1. row ***************************
    lock_id: 2856:109:4:6
lock_trx_id: 2856 lock_mode: X,GAP
  lock_type: RECORD
 lock_table: `test`.`t`
 lock_index: idx_t
 lock_space: 109
  lock_page: 4 lock_rec: 6 lock_data: 7
*************************** 2. row ***************************
    lock_id: 421383739058184:109:4:6
lock_trx_id: 421383739058184
  lock_mode: S
  lock_type: RECORD
 lock_table: `test`.`t`
 lock_index: idx_t
 lock_space: 109
  lock_page: 4
   lock_rec: 6
  lock_data: 7
2 rows in set (0.000 sec)

lock_mode為"X+GAP",表示next-key lock算法。其中lock_data值為7,表示鎖定了值為7的記錄,這是最大鎖定范圍邊界。lock_rec的值為6,表示鎖定了6行記錄,其中1,2,3,4,7共5行記錄是通過gap鎖鎖定的范圍,加上待插入的id=6(該行為key鎖鎖定),共鎖定6行記錄。

而如果使用的是大於號,由於操作任何一條記錄,它的下一個key都會被鎖定,這等價於鎖定了整個無窮區間,即實現了表鎖的功能。如下:

在會話1上執行:

# 首先回滾
rollback;
begin;
select * from t where id>10 lock in share mode;

在會話2執行:

insert into t values(0);
insert into t values(5);
insert into t values(100);

會發現任何插入都是阻塞的。即鎖定的范圍為(-∞,+∞),等價於鎖定了整張表。

但是如果使用的等於號,那么在查找索引的時候發現只需鎖定一條記錄和下一條記錄中間的范圍即可。

在會話1執行:

# 首先回滾
rollback;
begin;
select * from t where id=5 lock in share mode;

在會話2執行:

insert into t values(0);
insert into t values(10);

會發現上述插入都是允許的。

但如果插入id=6的記錄,則阻塞,因為鎖定的范圍為[5,7]區間。

也就是說,在有索引的情況下,如果是非具體的行鎖,那么就會將能掃描到的索引鍵值內的所有范圍加鎖。

下面測試沒有索引的情況。

2.無索引的情況

首先創建沒有索引的表,然后插入一些分隔的記錄。

create table ttt(id  int);
insert into ttt values(1),(2),(3),(4),(7),(8),(12),(15);

在會話1上執行:

begin;
select * from ttt where id=4  lock in share mode;

在會話2上執行:

insert into ttt values(5);
insert into ttt values(100);
insert into ttt values(0);

會發現不管是插入哪些記錄,都會被阻塞。因為沒有索引鍵值的時候,自動隱式創建索引會鎖定整個區間。查看下innodb的事務狀態。

mysql> show engine innodb status;
------------
TRANSACTIONS
------------
Trx id counter 15102
Purge done for trx's n:o < 15096 undo n:o < 0 state: running but idle
History list length 944
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 15066, not started
MySQL thread id 53, OS thread handle 0x7f5d57d47700, query id 1615 localhost root
---TRANSACTION 15065, not started
MySQL thread id 52, OS thread handle 0x7f5d57dc9700, query id 1590 localhost root
---TRANSACTION 15097, not started
MySQL thread id 51, OS thread handle 0x7f5d57ecd700, query id 1637 localhost root
---TRANSACTION 0, not started
MySQL thread id 50, OS thread handle 0x7f5d57e0a700, query id 1642 localhost root init
show engine innodb status
---TRANSACTION 15101, ACTIVE 3 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 360, 1 row lock(s)
MySQL thread id 49, OS thread handle 0x7f5d57d88700, query id 1641 localhost root update
insert into ttt values(0)
------- TRX HAS BEEN WAITING 3 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 190 page no 3 n bits 80 index `GEN_CLUST_INDEX` of table `test`.`ttt` trx id 15101 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
 0: len 8; hex 73757072656d756d; asc supremum;;

------------------
---TRANSACTION 15087, ACTIVE 215 sec
2 lock struct(s), heap size 360, 9 row lock(s)
MySQL thread id 43, OS thread handle 0x7f5d57f0e700, query id 1631 localhost root

可以發現,這時的鎖不是范圍鎖,因為沒有了locks gap,但卻仍然是行鎖而不是表鎖,只不過此時等價於表鎖。如下

mysql> select * from information_schema.innodb_locks\G
*************************** 1. row ***************************
    lock_id: 15102:190:3:1
lock_trx_id: 15102
  lock_mode: X
  lock_type: RECORD
 lock_table: `test`.`ttt`
 lock_index: GEN_CLUST_INDEX
 lock_space: 190
  lock_page: 3
   lock_rec: 1
  lock_data: supremum pseudo-record
*************************** 2. row ***************************
    lock_id: 15087:190:3:1
lock_trx_id: 15087
  lock_mode: S
  lock_type: RECORD
 lock_table: `test`.`ttt`
 lock_index: GEN_CLUST_INDEX
 lock_space: 190
  lock_page: 3
   lock_rec: 1
  lock_data: supremum pseudo-record

發現確實是行鎖而非表鎖。並且索引鍵值那里為"supermum pseudo-record",這表示鎖定的是"最大上界偽記錄",即鎖定的是無窮值。

沒索引的時候,哪怕查詢具體的行記錄都會鎖定整個區間,更不用說鎖定范圍(例如:where id>5)。其實它們的結果都是一樣的:鎖定整個區間。

4.4 innodb中的鎖等待超時

在innodb存儲引擎中,當出現鎖等待時,如果等待超時,將會結束事務,超時時長通過動態變量innodb_lock_wait_timeout值來決定,默認是等待50秒。關於鎖等待超時,可以直接在語句中設置超時時間。可以設置鎖等待超時時間的語句包括:wait n的n單位為秒,nowait表示永不超時。

ALTER TABLE tbl_name [WAIT n|NOWAIT] ...
CREATE ... INDEX ON tbl_name (index_col_name, ...) [WAIT n|NOWAIT] ...
DROP INDEX ... [WAIT n|NOWAIT]
DROP TABLE tbl_name [WAIT n|NOWAIT] ...
LOCK TABLE ... [WAIT n|NOWAIT]
OPTIMIZE TABLE tbl_name [WAIT n|NOWAIT]
RENAME TABLE tbl_name [WAIT n|NOWAIT] ...
SELECT ... FOR UPDATE [WAIT n|NOWAIT]
SELECT ... LOCK IN SHARE MODE [WAIT n|NOWAIT]
TRUNCATE TABLE tbl_name [WAIT n|NOWAIT]

超時后結束事務的方式有中斷性結束回滾性結束兩種方式,這也是通過變量來控制的,該變量為innodb_rollback_on_timeout,默認為off,即超時后不回滾,也即中斷性結束

mysql> show variables like "innodb%timeout";
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| innodb_flush_log_at_timeout | 1     |
| innodb_lock_wait_timeout    | 50    |
| innodb_rollback_on_timeout  | OFF   |
+-----------------------------+-------+


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM