1. 程式人生 > >MySQL innodb中各種SQL語句加鎖分析

MySQL innodb中各種SQL語句加鎖分析

utf8 mea 插入記錄 隔離 無法自動 change lac 進行 而不是

概要


Locking read( SELECT ... FOR UPDATE or SELECT ... LOCK IN SHARE MODE),UPDATE以及DELETE語句通常會在他掃描的索引所有範圍上加鎖,忽略沒有用到索引的那部分where語句。
舉個例子:

CREATE TABLE `test` (
  `id` int(11) NOT NULL DEFAULT ‘0‘,
  `name` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

select * from test where id > 3 and name <‘A‘ for update;

這條SQL語句的會將所有id>3的記錄進行加鎖,而不是id>3 and name <‘A‘ 進行加鎖,因為name上面沒有索引。

如果一個SQL通過二級索引進行掃描,並且在二級索引上設置了一個鎖,那麽innodb將會在對應的聚簇索引記錄上也加上一把鎖。

如果一個SQL語句無法通過索引進行Locking readUPDATEDELETE,那麽MySQL將掃描整個表,表中的每一行都將被鎖定(在RC級別,通過semi-consistent read,能夠提前釋放不符合條件的記錄,在RR級別,需要設置innodb_locks_unsafe_for_binlog為1,才能打開semi-consistent read

)。在某些場景下,鎖也不會立即被釋放。例如一個union查詢,生成 了一張臨時表,導致臨時表的行記錄和原始表的行記錄丟失了聯系,只能等待查詢執行結束才能釋放。

SQL分析


1.SELECT ... FROM 是一個快照讀,通過讀取數據庫的一個快照,不會加任何鎖,除非將隔離級別設置成了 SERIALIZABLE 。在 SERIALIZABLE 隔離級別下,如果索引是非唯一索引,那麽將在相應的記錄上加上一個共享的next key鎖。如果是唯一索引,只需要在相應記錄上加index record lock

2. SELECT ... FROM ... LOCK IN SHARE MODE 語句在所有索引掃描範圍的索引記錄上加上共享的next key

鎖。如果是唯一索引,只需要在相應記錄上加index record lock

3. SELECT ... FROM ... FOR UPDATE 語句在所有索引掃描範圍的索引記錄上加上排他的next key鎖。如果是唯一索引,只需要在相應記錄上加index record lock。這將堵塞其他會話利用SELECT ... FROM ... LOCK IN SHARE MODE 讀取相同的記錄,但是快照讀將忽略記錄上的鎖。

4. UPDATE ... WHERE ...語句在所有索引掃描範圍的索引記錄上加上排他的next key鎖。如果是唯一索引,只需要在相應記錄上加index record lock

UPDATE 操作修改主鍵記錄的時候,將在相應的二級索引上加上隱式的鎖。當進行重復鍵檢測的時候,將會在插入新的二級索引記錄之前,在其二級索引上加上一把共享鎖。

5. DELETE FROM ... WHERE ... 語句在所有索引掃描範圍的索引記錄上加上排他的next key鎖。如果是唯一索引,只需要在相應記錄上加index record lock

6. INSERT 語句將在插入的記錄上加一把排他鎖,這個鎖是一個index-record lock,並不是next-key鎖,因此就沒有gap 鎖,他將不會阻止其他會話在該條記錄之前的gap插入記錄。

在插入記錄之前,將會加上一種叫做 insert intention gapgap 鎖。這個 insert intention gap表示他有意向在這個index gap插入記錄,如果其他會話在這個index gap中插入的位置不相同,那麽將不需要等待。假設存在索引記錄4和7,會話A要插入記錄5,會話B要插入記錄6,每個會話在插入記錄之前都需要鎖定4和7之間gap,但是他們彼此不會互相堵塞,因為插入的位置不相同。

如果出現了重復鍵錯誤,將在重復鍵上加一個共享鎖。如果會話1插入一條記錄,沒有提交,他會在該記錄上加上排他鎖,會話2和會話3都嘗試插入該重復記錄,那麽他們都會被堵塞,會話2和會話3將嘗試在該記錄上加一個共享鎖。如果此時會話1回滾,將發生死鎖。

例子如下:
表結構:
CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;

Session 1:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

Session 2:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

Session 3:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

Session 1:

ROLLBACK;

為什麽會發生死鎖呢?當會話1進行回滾的時候,記錄上的排他鎖釋放了,會話2和會話3都獲得了共享鎖。然後會話2和會話3都想要獲得排他鎖,進而發生了死鎖。

還有一個類似的例子

Session 1:

START TRANSACTION;
DELETE FROM t1 WHERE i = 1;

Session 2:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

Session 3:

START TRANSACTION;
INSERT INTO t1 VALUES(1);

Session 1:

COMMIT;

會話1在該記錄上擁有一把排他鎖,會話2和會話3都碰到了重復記錄,因此都在申請共享鎖。當會話1提交之後,會話1釋放了排他鎖,之後的會話2會話3先後獲得了共享鎖,此時他們發生了死鎖,因為會話2和會話3都無法或者排他鎖,因為彼此都占用了該記錄的共享鎖。

7. INSERT ... ON DUPLICATE KEY UPDATE 和普通的INSERT並不相同。如果碰到重復鍵值,INSERT ... ON DUPLICATE KEY UPDATE 將在記錄上加排他的 next-key鎖。

8. REPLACE 在沒有碰到重復鍵值的時候和普通的INSERT是一樣的,如果碰到重復鍵,將在記錄上加一個排他的 next-key鎖。

9. INSERT INTO T SELECT ... FROM S WHERE ... 語句在插入T表的每條記錄上加上 index record lock 。如果隔離級別是 READ COMMITTED, 或者啟用了 innodb_locks_unsafe_for_binlog 且事務隔離級別不是SERIALIZABLE,那麽innodb將通過快照讀取表S(no locks)。否則,innodb將在S的記錄上加共享的next-key鎖。

CREATE TABLE ... SELECT ... INSERT INTO T SELECT ... FROM S WHERE ... 一樣,在S上加共享的next-key鎖或者進行快照讀取((no locks)

10. REPLACE INTO t SELECT ... FROM s WHERE ... UPDATE t ... WHERE col IN (SELECT ... FROM s ...) 中的select 部分將在表s上加共享的next-key鎖。

11. 當碰到有自增列的表的時候,innodb在自增列的索引最後面加上一個排他鎖,叫AUTO-INC table lockAUTO-INC table lock會在語句執行完成後進行釋放,而不是事務結束。如果AUTO-INC table lock被一個會話占有,那麽其他會話將無法在該表中插入數據。innodb可以預先獲取sql需要多少自增的大小,而不需要去申請鎖,更多設置請參考參數innodb_autoinc_lock_mode.

12.如果一張表的外鍵約束被啟用了,任何在該表上的插入、更新、刪除都將需要加共享的record-level locks來檢查是否滿足約束。如果約束檢查失敗,innodb也會加上共享的record-level locks

13. lock tables 是用來加表級鎖,但是是MySQL的server層來加這把鎖的。當innodb_table_locks = 1 (the default) 以及 autocommit = 0的時候,innodb能夠感知表鎖,同時server層了解到innodb已經加了row-level locks。否則,innodb將無法自動檢測到死鎖,同時server無法確定是否有行級鎖,導致當其他會話占用行級鎖的時候還能獲得表鎖。

鎖測試註意點


1. 當使用begin開啟一個事務的時候,之後的查詢並不是獲取的begin 命令的時間點快照,而且begin命令之後第一個查詢的時間點快照。

CREATE TABLE T2 (id INT,name varchar(10) ,PRIMARY KEY (id)) ENGINE = InnoDB;
INSERT INTO T2 VALUES(1,‘zhangsan‘),(2,‘lisi‘);


SESS1:                      SESS2:
BEGIN;
                            BEGIN;
                            INSERT INTO T2 values(3,‘wangwu‘);
                            COMMIT;
SELECT * FROM T2;
+----+----------+
| id | name     |
+----+----------+
|  1 | zhangsan |
|  2 | lisi     |
|  3 | wangwu   |
+----+----------+
                          

在可重復讀的情況下,為什麽會出現這樣的情況呢?

原因就是SESS1 沒有BEGIN之後開啟一個查詢,導致SESS1的select * from t2 查詢的快照是執行該SQL的快照,而不是BEGIN那個時間點的快照,而此時,SESS2已經提交。

2. 我們來看一個例子:

CREATE TABLE T3 (id INT,name varchar(10) ,PRIMARY KEY (id)) ENGINE = InnoDB;
INSERT INTO T3 VALUES(1,‘a‘),(2,‘b‘),(3,‘c‘);


SESS1:                      SESS2:
BEGIN;
SELECT * FROM T3;
+----+------+
| id | name |
+----+------+
|  1 | a    |
|  2 | b    |
|  3 | c    |
+----+------+
                            BEGIN;
                            INSERT INTO T3 values(4,‘a‘);
                            COMMIT;
SELECT * FROM T3;
+----+------+
| id | name |
+----+------+
|  1 | a    |
|  2 | b    |
|  3 | c    |
+----+------+
UPDATE T3 SET NAME=‘aa‘ where name =‘a‘;
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2  Changed: 2  Warnings: 0
SELECT * FROM T3;                       
+----+------+
| id | name |
+----+------+
|  1 | aa   |
|  2 | b    |
|  3 | c    |
|  4 | aa   |
+----+------+
                            SELECT * FROM T3;                       
                            +----+------+
                            | id | name |
                            +----+------+
                            |  1 | a    |
                            |  2 | b    |
                            |  3 | c    |
                            |  4 | a    |
                            +----+------+

在可重復讀的情況下,為什麽SESS1先開啟事務的情況下,還能更新會話2後來提交的數據呢?然後之後的select還能看到更新後的數據?我們查看下官方解釋。

A consistent read means that InnoDB uses multi-versioning to present to a query a snapshot of the database at a point in time. The query sees the changes made by transactions that committed before that point of time, and no changes made by later or uncommitted transactions. The exception to this rule is that the query sees the changes made by earlier statements within the same transaction. This exception causes the following anomaly: If you update some rows in a table, a SELECT sees the latest version of the updated rows, but it might also see older versions of any rows. If other sessions simultaneously update the same table, the anomaly means that you might see the table in a state that never existed in the database.

默認情況下,innodb使用MVCC進行快照讀。查詢只能查看到之前提交的事務更改的數據,之後提交的數據或者沒有提交的事務數據是沒辦法看到的。但是這裏有個例外就是同一個事務的查詢能看到同一個事務裏面的更改。因此當SESS1 在進行UPDATE的時候,會進行當前讀,也就是讀取所有已經提交的數據,相當於讀取的是select * from t3 where where name =‘a‘ for update;的結果集,然後進行UPDATE。之後的select * from t3 where where name =‘a‘看到的就是當前UPDATE之後的結果了。

參考資料:http://docs.fordba.com/mysql/refman-5.6-en/innodb-storage-engine.html#innodb-locks-set

MySQL innodb中各種SQL語句加鎖分析