MySQL -- INSERT語句的鎖
摘要:
CREATE TABLE `t` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`c` INT(11) DEFAULT NULL,
`d` INT(11) DEFAULT NULL,
PRIMARY KEY (`id`),
...
CREATE TABLE `t` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `c` INT(11) DEFAULT NULL, `d` INT(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `c` (`c`) ) ENGINE=InnoDB; INSERT INTO t VALUES (null,1,1); INSERT INTO t VALUES (null,2,2); INSERT INTO t VALUES (null,3,3); INSERT INTO t VALUES (null,4,4); CREATE TABLE t2 LIKE t;
操作序列
時刻 | session A | session B |
---|---|---|
T1 | BEGIN; | |
T2 | INSERT INTO t2(c,d) SELECT c,d FROM t; | |
T3 | INSERT INTO t VALUES (-1,-1,-1); (Blocked) |
-- T3時刻 mysql> SELECT locked_index,locked_type,waiting_lock_mode,blocking_lock_mode FROM sys.innodb_lock_waits WHERE locked_table='`test`.`t`'; +--------------+-------------+-------------------+--------------------+ | locked_index | locked_type | waiting_lock_mode | blocking_lock_mode | +--------------+-------------+-------------------+--------------------+ | PRIMARY| RECORD| X,GAP| S| +--------------+-------------+-------------------+--------------------+
- T2時刻,session B會在表t加上
PRIMARY:Next-Key Lock:(-∞,1]
- 如果沒有鎖的話,就可能會出現session B的 INSERT語句先執行 ,但對應的 binlog後寫入 的情況
-
binlog_format=STATEMENT
,binlog裡面的語句序列如下INSERT INTO t VALUES (-1,-1,-1) INSERT INTO t2(c,d) SELECT c,d FROM t
- 這個語句傳到備庫執行,就會把id=-1這一行也會寫到t2, 主備不一致
-
INSERT迴圈寫入
非迴圈寫入
mysql> EXPLAIN INSERT INTO t2(c,d) (SELECT c+1,d FROM t FORCE INDEX(c) ORDER BY c DESC LIMIT 1); +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------+ | id | select_type | table | partitions | type| possible_keys | key| key_len | ref| rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------+ |1 | INSERT| t2| NULL| ALL| NULL| NULL | NULL| NULL | NULL |NULL | NULL| |1 | SIMPLE| t| NULL| index | NULL| c| 5| NULL |1 |100.00 | NULL| +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------+ # Time: 2019-03-15T04:55:55.315664Z # User@Host: root[root] @ localhost []Id:2 # Query_time: 0.003300Lock_time: 0.000424 Rows_sent: 0Rows_examined: 1 SET timestamp=1552625755; INSERT INTO t2(c,d) (SELECT c+1,d FROM t FORCE INDEX(c) ORDER BY c DESC LIMIT 1);
- 加鎖範圍為在表t上
c:Next-Key Lock:(3,4]
+c:Next-Key Lock:(4,+∞]
- 執行流程比較簡單,從表t中按索引c倒序掃描第一行,拿到結果後寫入到表t2,整個語句的掃描行數為1
迴圈寫入
-- MySQL 5.7上執行 mysql> EXPLAIN INSERT INTO t(c,d) (SELECT c+1,d FROM t FORCE INDEX(c) ORDER BY c DESC LIMIT 1); +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-----------------+ | id | select_type | table | partitions | type| possible_keys | key| key_len | ref| rows | filtered | Extra| +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-----------------+ |1 | INSERT| t| NULL| ALL| NULL| NULL | NULL| NULL | NULL |NULL | NULL| |1 | SIMPLE| t| NULL| index | NULL| c| 5| NULL |1 |100.00 | Using temporary | +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-----------------+ mysql> SHOW STATUS LIKE '%Innodb_rows_read%'; +------------------+-------+ | Variable_name| Value | +------------------+-------+ | Innodb_rows_read | 13| +------------------+-------+ mysql> INSERT INTO t(c,d) (SELECT c+1,d FROM t FORCE INDEX(c) ORDER BY c DESC LIMIT 1); Query OK, 1 row affected (0.00 sec) Records: 1Duplicates: 0Warnings: 0 mysql> SHOW STATUS LIKE '%Innodb_rows_read%'; +------------------+-------+ | Variable_name| Value | +------------------+-------+ | Innodb_rows_read | 17| +------------------+-------+ # Time: 2019-03-15T05:10:38.603323Z # User@Host: root[root] @ localhost []Id:2 # Query_time: 0.004470Lock_time: 0.000184 Rows_sent: 0Rows_examined: 5 SET timestamp=1552626638; INSERT INTO t(c,d) (SELECT c+1,d FROM t FORCE INDEX(c) ORDER BY c DESC LIMIT 1);
-
Using temporary
表示用到了 臨時表 ,執行過程中,需要把表t的內容讀出來,寫入臨時表 - 實際上,
EXPLAIN
結果裡的rows=1
是因為受到了LIMIT 1
的影響 - 語句執行前後,
Innodb_rows_read
的值增加了4,因為臨時表預設使用的是 Memory引擎- 這4行資料查的是表t,即對錶t做了 全表掃描
- 執行流程
- 建立臨時表,表裡有兩個欄位
c
和d
- 按照索引c掃描表t,依次取出c=4,3,2,1,然後 回表 ,讀到c和d的值寫入臨時表
- 此時,
Rows_examined=4
- 此時,
- 由於有
LIMIT 1
,所以只會取臨時表的第一行,再插入到表t- 此時,
Rows_examined=5
- 此時,
- 建立臨時表,表裡有兩個欄位
- 該語句會導致在表t上做 全表掃描 ,並且會給索引c上的所有間隙都加上
Share Next-Key Lock
- 在這個語句執行期間,其它事務不能在這個表上插入資料
- 需要臨時表的原因
- 一邊遍歷資料,一邊更新資料
- 如果讀出來的資料直接寫回原表,可能在遍歷過程中,讀到剛剛插入的記錄
- 新插入的記錄如果參與計算邏輯,就會與原語義不符
優化方案
CREATE TEMPORARY TABLE temp_t(c INT,d INT) ENGINE=Memory; -- Rows_examined=1 INSERT INTO temp_t (SELECT c+1, d FROM t FORCE INDEX(c) ORDER BY c DESC LIMIT 1); -- Rows_examined=1 INSERT INTO t(c,d) SELECT * FROM temp_t; DROP TABLE temp_t;
INSERT唯一鍵衝突
時刻 | session A | session B |
---|---|---|
T0 | SELECT * FROM t; | |
T1 | INSERT INTO t VALUES (10,10,10); | |
T2 | BEGIN; | |
T3 | INSERT INTO t VALUES (11,10,10); (Duplicate entry ‘10’ for key ‘c’) |
|
T4 | INSERT INTO t VALUES (12,9,9); (Blocked) |
-- T0時刻 mysql> SELECT * FROM t; +----+------+------+ | id | c| d| +----+------+------+ |1 |1 |1 | |2 |2 |2 | |3 |3 |3 | |4 |4 |4 | |5 |5 |4 | +----+------+------+ mysql> SELECT locked_index,locked_type,waiting_lock_mode,blocking_lock_mode FROM sys.innodb_lock_waits WHERE locked_table='`test`.`t`'; +--------------+-------------+-------------------+--------------------+ | locked_index | locked_type | waiting_lock_mode | blocking_lock_mode | +--------------+-------------+-------------------+--------------------+ | c| RECORD| X,GAP| S| +--------------+-------------+-------------------+--------------------+
- session A要執行的INSERT語句,發生唯一鍵衝突,並不是簡單地報錯返回,還需要在 衝突的索引 上加鎖
- 一個
Next-Key Lock
由它的 右邊界 定義的,即是c:Shared Next-Key Lock:(5,10]
INSERT死鎖
時刻 | session A | session B | session C |
---|---|---|---|
T0 | TRUNCATE t; | ||
T1 | BEGIN; INSERT INTO t VALUES (null,5,5); |
||
T2 | INSERT INTO t VALUES (null,5,5); | INSERT INTO t VALUES (null,5,5); | |
T3 | ROLLBACK; | Deadlock found |
- 在T1時刻,session A執行
INSERT
語句,在索引c=5
上加上 行鎖 (索引c是 唯一索引 ,可以退化為 行鎖 ) - 在T2時刻,session B和session C執行相同的
INSERT
語句,發現 唯一鍵衝突 , 等待加上 讀鎖 - 在T3時刻,session A執行
ROLLBACK
語句,session B和session C都試圖繼續插入執行操作,都要加上 寫鎖- 但兩個session都要等待對方的 讀鎖 ,所以就出現了死鎖
INSERT INTO…ON DUPLICATE KEY
TRUNCATE T; INSERT INTO t VALUES (1,1,1),(2,2,2); -- 如果有多個列違反唯一性約束,按照索引的順序,修改跟第一個索引衝突的行 -- 2 rows affected,insert和update都認為自己成功了,update計數加1,insert計數也加1 INSERT INTO t VALUES (2,1,100) ON DUPLICATE KEY UPDATE d=100; Query OK, 2 rows affected (0.01 sec) Records: 2Duplicates: 0Warnings: 0 mysql> SELECT * FROM t; +----+------+------+ | id | c| d| +----+------+------+ |1 |1 |1 | |2 |2 |100 | +----+------+------+