MySQL lock detailed explanation and case analysis
- 1. An update statement
- 2. Introduction to MySQL locks
- 3. Global lock
- 4. MySQL table-level locks (all implemented by the server layer)
- 5. MySQL row-level locks (fine granularity)
- 6. Practical analysis of row locks
-
- 1) Combination 1: id primary key + RC
- 2) Combination 2: ID unique index + RC
- 3) Combination three: id non-unique index + RC
- 4) Combination four: id no index + RC
- 5) Combination five: id primary key + RR
- 6) Combination six: id unique index + RR
- 7) Combination 7: id non-unique index + RR
- 8) Combination eight: id no index + RR
- 9) Combination nine: Serializable
- 7. Locking analysis of a complex SQL
- 8. Deadlock Principle and Analysis
- Nine excellent links
1. An update statement
2. Introduction to MySQL locks
-
In terms of lock granularity, MySQL mainly contains three types (levels) of locking mechanisms:
全局锁
: The entire database is locked. Implemented by the SQL layer of MySQL
表级锁
: a certain table is locked. Implemented by the SQL layer of MySQL
行级锁
: it locks a certain row of data, and may also lock gaps between rows. Implemented by some storage engines, such as InnoDB. -
According to the function of the lock, it is divided into: shared lock and exclusive lock .
Shared Locks (S locks):
1. Compatibility: records with S locks are allowed to add S locks to other transactions, and X locks are not allowed to be added to other transactions.
2. Locking method: select...lock in share modeExclusive Locks (X locks):
1. Compatibility: records with X locks are added, and other transactions are not allowed to add S locks or X locks
2. Locking method: select...for update
3. Global lock
The global lock locks the entire database instance. After the lock is locked, the entire instance is in a read-only state. Subsequent MDL write statements, DDL statements, and transaction commit statements that have been updated will be blocked. Its typical usage scenario is to make a logical backup of the entire database and lock all tables to obtain a consistent view and ensure data integrity.
The command to add a global lock is:
flush tables with read lock
The command to release the global lock is:
unlock tables;
Unlocking method two
View locked processes
If the MySQL database is locked, you can use the following command to view the locked process:
SHOW FULL PROCESSLIST;
this command will display all current database connections and their status, including the SQL statement being executed, execution time and other information. By viewing this information, you can find locked processes and their related information.
Kill locked process
After finding the locked process, you can use the following command to kill the process:
KILL <process ID>;
where the process ID is the process ID displayed in the SHOW FULL PROCESSLIST command. By killing the locked process, the MySQL database can be unlocked.
Global lock demonstration
1.Environment preparation
CREATE TABLE mylock (
id int(11) NOT NULL AUTO_INCREMENT,
NAME varchar(20) DEFAULT NULL,
PRIMARY KEY (id)
);
INSERT INTO mylock (id,NAME) VALUES (1, 'a');
INSERT INTO mylock (id,NAME) VALUES (2, 'b');
INSERT INTO mylock (id,NAME) VALUES (3, 'c');
INSERT INTO mylock (id,NAME) VALUES (4, 'd');
2. Global lock demonstration
Reply 1 | Reply 2 |
---|---|
1. Lock the database![]() |
2. Create a new session to perform query operations![]() |
none | 3. Execute the delete statement![]() |
4. Release the lock![]() |
5. Execute the delete statement again![]() |
4. MySQL table-level locks (all implemented by the server layer)
1. Introduction to table-level locks
There are four types of table-level locks in MySQL:
1. Table read and write locks.
2. Meta data lock (meta data lock, MDL).
3. Intention Locks (InnoDB)
4. AUTO-INC Locks
2. Table read S, write lock X
1) Table lock related commands
- Contention state variables for table-level locking implemented in MySQL:
show status like 'table%';
- table_locks_immediate: The number of table-level locks generated;
- table_locks_waited: The number of waits due to table-level lock contention
-
Table locks have two forms
Table shared read lock (Table Read Lock)
table exclusive write lock (Table Write Lock) -
Manually add table lock
lock table table name read(write), table name 2 read(write), others;
1. Table-level shared lock
lock table table name read;
read operations can be shared, but no operations can be performed.
2. Table-level exclusive lock
lock table table name write; -
Check table lock status
show open tables;
-
Remove table lock:
unlock tables;
2) Table lock demonstration
1. Table-level shared lock (read lock)
Can be shared for reading, but cannot perform write operations
Once a shared lock is added to this table, other sessions can also add shared locks to this table; shared locks can coexist, you can add a lock, and I can also add a lock. Because it is a shared read lock, after the lock is locked, the table can be read. The current transaction cannot perform write operations, and other transactions cannot perform write operations. All read locks need to be released.
lock table 表名 read;
Reply 1 | Reply 2 |
---|---|
1. Add shared lock![]() |
none |
2. Query data![]() |
none |
3. New data: prompts that there is a read lock and cannot be updated![]() |
4. Create a new conversation and add a lock![]() |
none | 5. Query data![]() |
none | 6. New data: prompts that there is a read lock and cannot be updated![]() |
none | 7. Release the read lock![]() |
none | 8. Execute the new data again, and it is always in a waiting state because there is a read lock in session one.![]() |
9. Release the lock as soon as you reply![]() |
10. New data added in Reply 2: Success![]() |
2. Table-level exclusive lock
An exclusive lock is added to the table in a session/transaction, and the current session can read and write operations on the table; other sessions will be blocked when reading and performing operations on the table, and need to wait for the lock to be released in the locked transaction.
lock table 表名 write;
The table is in an exclusive state.
Write lock demonstration
Reply 1 | Reply 2 |
---|---|
1. Add a write lock to the mylock table in Reply 1![]() |
none |
2. The current session can perform query + update + new operations on the locked table.![]() |
|
3. The second session is blocked when querying (modifying/adding) and needs to wait for the lock to be released.![]() |
|
4. Release the lock![]() |
5. Reply 2 obtained the lock query successfully![]() |
3. Metadata lock
1) Introduction to metadata lock
When a table is queried in a transaction and other sessions are not allowed to modify the table structure, a metadata lock is added to the table.
MDL does not need to be used explicitly, it will be added automatically when accessing a table. The function of MDL is to ensure the correctness of reading and writing. You can imagine that if a query is traversing the data in a table, and during execution another thread makes changes to the table structure and deletes a column, then the results obtained by the query thread do not match the table structure, which is definitely not possible. .
Therefore, MDL was introduced in MySQL version 5.5. When adding, deleting, modifying, and querying a table, add MDL read locks; when making structural changes to the table, add MDL write locks .
Read locks are not mutually exclusive, so you can have multiple threads adding, deleting, modifying, and querying a table at the same time.。
Read-write locks and write locks are mutually exclusive to ensure the security of table structure changes.. Therefore, if two threads want to add fields to a table at the same time, one of them will have to wait for the other to finish before it can start execution .
2) Metadata lock demonstration
1、session1: begin;--开启事务
select * from mylock;--加MDL读锁
2、session2: alter table mylock add f int; -- 修改阻塞
3、session1:commit; --提交事务 或者 rollback 释放读锁
4、session2:Query OK, 0 rows affected (38.67 sec) --修改完成
Records: 0 Duplicates: 0 Warnings: 0
Reply 1 | Reply 2 |
---|---|
1. session1: begin;–start transaction![]() |
none |
1.1 select * from mylock; – add MDL read lock![]() |
2. session2: alter table mylock add age int; – Modify blocking![]() |
3. session1: commit; – commit the transaction or rollback to release the read lock![]() |
4. Execution successful![]() |
4. AUTO-INC Locks
AUTO-INC lock is a special table-level lock that occurs when a transactional insert operation involving the AUTO_INCREMENT column occurs.
5. MySQL row-level locks (fine granularity)
It is required to use the InnoDB engine
1. Introduction to row-level locks
MySQL's row-level locks are implemented by the storage engine . Here we mainly explain InnoDB's row-level locks . InnoDB row locks are implemented
by locking index entries on the index . Therefore, the characteristics of InnoDB row lock implementation mean:InnoDB only uses row-level locks for data retrieved through index conditions, otherwise InnoDB will use table locks!
-
InnoDB's row-level locks are divided into four types according to the locking scope:
记录锁(Record Locks):
Lock a record in the index.
间隙锁(Gap Locks):
Either lock the value in the middle of the index record, or lock the value before the first index record or the value after the last index record.
临键锁(Next-Key Locks):
It is a combination of a record lock on the index record and a gap lock before the index record (gap lock + record lock).
插入意向锁(Insert Intention Locks):
The lock on the record id added during the insert operation. -
InnoDB's row-level locks are divided into two types according to their functions:
共享锁(S):
Allows one transaction to read a row, preventing other transactions from obtaining an exclusive lock on the same data set.
排他锁(X):
Allow transactions that obtain exclusive locks to update data and prevent other transactions from obtaining shared read locks and exclusive write locks on the same data set.
For UPDATE
the, DELETE
and INSERT
statements, InnoDB
an exclusive lock (X) will be automatically added to the involved data set; for the ordinary SELECT
statement, InnoDB
no lock will be added. The transaction can add shared locks or exclusive locks to the record set through the following statements.
Manually add shared lock(s):
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
Add exclusive lock (x) manually:
SELECT * FROM table_name WHERE ... FOR UPDATE
2. Record Locks
Use record locks when updating based on primary key values.
(1) Record lock, only locks one row of the index record and locks a single index record.
(2) Record lock always locks the index, not the record itself. Even if there is no index on the table, innodb will create a hidden clustered primary key index in the background, and it is this hidden clustered primary key index that is locked.
Therefore, when a SQL statement does not use any index, an X lock will be added after each aggregate index. This is similar to a table lock, but in principle it should be completely different from a table lock.
-- 共享记录锁
select * from table where id = 1 lock in share mode;
-- 排他记录锁
select * from table where id = 1 for udpate;
Shared record lock demo
If you add a shared record lock to a certain record, other sessions can also add a shared record lock to this record, and you can also read this record; but you cannot write, and you cannot add an exclusive lock to this record ( will be in a blocked state).
During the demonstration, we turned on the transaction so that we can easily see the effect. If the transaction is not enabled, the lock will be automatically released after this statement is executed, and the effect will be invisible.
Reply 1
-- 1.开启事务
BEGIN;
-- 2.查询操作
SELECT * FROM `mylock`;
-- 3.给id=2的记录添加共享记录锁
select * from mylock where id = 2 lock in share mode;
-- 4.本地回话可以进行写操作
UPDATE mylock SET name='李四' WHERE id=2
-- 4.1.查询结果,看见更新成功
SELECT * FROM `mylock`;
-- 11.再次查询结果,看见查询的数据和步骤5一致,
--而不是回话2中修改后的,这是正确的保证一个事务中数据不可重复读
SELECT * FROM `mylock`;
-- 12提交事务
COMMIT;
-- 13 再次查询,数据最新的
SELECT * FROM `mylock`;
Reply 2
-- 回话2
-- 5.查询id=2数据成功
select * from mylock where id = 2
-- 6.加记录共享锁成功
select * from mylock where id = 2 lock in share mode;
-- 7.加记录排他锁阻塞
select * from mylock where id = 2 for update;
-- 8.更新id=2的数据阻塞
UPDATE mylock set name='李武' WHERE id =2;
-- 9.更新其他记录成功
UPDATE mylock set name='李白' WHERE id =3;
-- 10.查询记录,由于回话一中并没有提交id=2的修改记录,查询到的仍是旧值
SELECT * FROM mylock;
Reply 1 | Reply 2 |
---|---|
BEGIN; – 1. Start transaction | |
SELECT * FROM mylock ; – 2. Query operation![]() |
|
select * from mylock where id = 2 lock in share mode; – 3. Add a shared record lock to the record with id=2![]() |
|
UPDATE mylock SET name='李思' WHERE id=2 – 4. Local calls can perform write operations![]() |
|
SELECT * FROM mylock ;-- 4.1. Query the results and see that the update is successful![]() |
select * from mylock where id = 2 – 5. Query id=2 data successfully![]() |
none | select * from mylock where id = 2 lock in share mode; – 6. Add record shared lock successfully![]() |
none | select * from mylock where id = 2 for update; – 7. Failed to add record exclusive lock![]() |
none | UPDATE mylock set name='Li Wu' WHERE id =2; – 8. Failed to update data with id=2![]() |
none | UPDATE mylock set name=‘李白’ WHERE id =3; – 9.更新其他记录成功![]() |
无 | SELECT * FROM mylock; – 10.查询记录,由于回话一中并没有提交id=2的修改记录,查询到的仍是旧值![]() |
SELECT * FROM mylock ; – 11.再次查询结果,看见查询的数据和步骤4.1一致,而不是回话2中修改后的,这是正确的保证一个事务中数据不可重复读![]() |
无 |
COMMIT;-- 12提交事务 SELECT * FROM mylock ; – 13 再次查询发现结果已是最新![]() |
排他记录锁演示
对某一条记录添加了排它锁,同一回话中可以写操作。在其他回话中可以读这个条记录,但是不能进行写,不能对这条记录加排它锁或者是共享锁(会处于阻塞状态)
回话一
-- 1.开启事务
BEGIN;
-- 2.查询操作
SELECT * FROM `mylock`;
-- 3.给id=2的记录添加排它锁
select * from mylock where id = 2 for UPDATE;
-- 4.id=2的记录更新操作成功
UPDATE mylock SET name='张丹' WHERE id=2;
-- 9 提交事务,当前排它锁释放
COMMIT;
回话二
-- 5.加共享锁处于阻塞
select * from mylock where id = 2 lock in share mode;
-- 6.加排它锁锁处于阻塞
select * from mylock where id = 2 for UPDATE;
-- 7.更新id=2记录处于阻塞
UPDATE mylock SET name='白冰' WHERE id=2;
-- 8.查询id=2的记录成功
SELECT * FROM `mylock` WHERE id=2;
回话1 | 回话2 |
---|---|
– 1.开启事务 BEGIN;-- 2.查询操作 SELECT * FROM mylock ;![]() |
|
– 3.给id=2的记录添加排它锁 select * from mylock where id = 2 for UPDATE; ![]() |
|
– 4.id=2的记录更新操作成功 UPDATE mylock SET name=‘张丹’ WHERE id=2;![]() |
|
无 | – 5.加共享锁处于阻塞select * from mylock where id = 2 lock in share mode;![]() |
无 | – 6.加排它锁锁处于阻塞select * from mylock where id = 2 for UPDATE; ![]() |
无 | – 7.更新id=2记录处于阻塞UPDATE mylock SET name=‘白冰’ WHERE id=2;![]() |
无 | – 8.查询id=2的记录成功SELECT * FROM mylock WHERE id=2;![]() |
– 9 提交事务,当前排它锁释放 COMMIT; |
3、间隙锁(Gap Locks)
在记录和记录之间的范围区间就是间隙,间隙锁就是加区间之上的,防止插入数据。目的就是防止幻读。
(1)区间锁, 仅仅锁住一个索引区间(开区间,不包括双端端点)。
(2)在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录 本身。
(3)间隙锁可用于防止幻读,保证索引间的不会被插入数据
session1:
begin;
select * from t1_simple where id > 4 for update;
--------------------------------------------------------- session2:
insert into t1_simple values (7,100); --阻塞
insert into t1_simple values (3,100); --成功
4、临键锁(Next-Key Locks)
就是记录锁+间隙锁
是一个左开右闭的区间。
(1,3],(3,5]…
在InnoDB中默认使用临键锁,当满足某些条件时,临键锁会退化。
(1)record lock + gap lock
, 左开右闭区间,例如(5,8]。
(2)默认情况下,innodb
使用next-key locks
来锁定记录。select … for update
(3)但当查询的索引含有唯一属性
的时候,Next-Key Lock
会进行优化,将其降级为Record Lock
,即仅锁住索引本身,不是范围。
(4)Next-Key Lock
在不同的场景中会退化:
案例演示
CREATE TABLE `t1_simple` (
`id` int(11) NOT NULL,
`pubtime` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_pu`(`pubtime`) USING BTREE ) ENGINE = InnoDB;
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (1, 10);
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (3, 3);
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (5, 100);
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (10, 1);
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (20, 5);
INSERT INTO `t1_simple`(`id`, `pubtime`) VALUES (100, 20);
(1) 排他记录锁:使用主键索引精确匹配(=),且记录存在。
session1执行:
begin;
-- 临键锁区间(10,20],(20,100]
select * from t1_simple where id = 20 for update;
session2执行:
-- 16和25在临键锁区间(10,20],(20,100]内,因为有临建锁会阻塞
insert into t1_simple values (16, 19); --阻塞
insert into t1_simple values (25, 50); --阻塞
-- 退化成排它锁,在其他回话中不能对这条记录加排它锁
select * from t1_simple where id = 20 for update; --阻塞
insert into t1_simple values (4, 101); --成功
(2)间隙锁: 使用主键索引精确匹配(=),且记录不存在。
session1执行:
BEGIN;
-- 6在(5,10)之间,锁为间隙锁
SELECT * FROM t1_simple WHERE id =6 FOR UPDATE;
session2执行:
--因为回话一给(5,10)添加了间隙锁。回话二中插入id=6数据时会阻塞
INSERT INTO `t1_simple` VALUES (6, 33);
--成功
INSERT INTO `t1_simple` VALUES (28, 32);
(3)临键锁: 使用主键索引精确匹配(< 和>)
加了临建锁,在范围值内,插入数据会阻塞。
session1执行:
BEGIN;
-- 添加临键锁
SELECT * FROM `t1_simple` WHERE id<=5 FOR UPDATE;
session2执行:
INSERT INTO `t1_simple` VALUES (2, 33); -- 阻塞
INSERT INTO `t1_simple` VALUES (5, 92); -- 阻塞
INSERT INTO `t1_simple` VALUES (18, 72); -- 成功
5、意向锁 (Intention Locks)(表级锁)
意向锁IX、IS,当数据库中某条记录上有行锁时,需要在表上增加一个标志位,代表当前表中某行记录上有行锁。
通俗的来说:
假如某行记录上有共享锁(S)那么表上标志位会变成意向共享锁(IS)
假如某行记录上有排它锁(X)那么表上标志位会变成意向排它锁(IX)
假如表中不同行记录上有共享锁(S)和排它锁(X)那么表上标志位会变成意向共享锁(IS)和意向排它锁(IX)
总结:
标记当前表中是否有行锁的存在。
行锁:S 意向锁:IS
行锁:X 意向锁:IX
1)意向锁介绍
InnoDB也实现了表级锁,也就是意向锁
,意向锁是mysql内部使用的,不需要用户干预。意向锁和行锁可以共存,意向锁的主要作用是为了【全表更新数据】时的性能提升。否则在全表更新数据时,需要先检索该范是否某些记录上面有行锁。
- 表明“某个事务正在某些行持有了锁、或该事务准备去持有锁”
- 意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
- 例子:事务A修改user表的记录r,会给记录r上一把行级的排他锁(X),同时会给user表上一把意向排他锁(IX),这时事务B要给user表上一个表级的排他锁就会被阻塞。意向锁通过这种方式实现了行锁和表锁共存且满足事务隔离性的要求。
- 1)意向共享锁(IS锁):事务在请求S锁前,要先获得IS锁
2)意向排他锁(IX锁):事务在请求X锁前,要先获得IX锁
2)意向锁的作用
当我们需要给表加一个排他锁时,需要根据意向锁去判断表中有没有数据行被锁定(行锁);
(1)如果意向锁是行锁,则需要遍历每一行数据去确认;
(2)如果意向锁是表锁,则只需要判断一次即可知道有没数据行被锁定,提升性能。
3)意向锁和共享锁、排他锁的兼容关系
下图表示意向锁和表共享锁、表排他锁的兼容关系
意向锁相互兼容,因为IX、IS只是表明申请更低层次级别元素(比如 page、记录)的X、S操作。
因为上了表级S锁后,不允许其他事务再加X锁,所以表级S锁和X、IX锁不兼容。
上了表级X锁后,会修改数据,所以表级X锁和 IS、IX、S、X(即使是行排他锁,因为表级锁定的行肯定包括行级锁定的行,所以表级X和IX、行级X)不兼容。
注意: 上了行级X锁后,行级X锁不会因为有别的事务上了IX而堵塞,一个mysql是允许多个行级X锁同时存在的,只要他们不是针对相同的数据行。
6、行锁加锁规则
1)主键索引
等值查询
(1)命中记录,加记录锁。
(2)未命中记录,加间隙锁。范围查询
(1)没有命中任何一条记录时,加间隙锁。
(2)命中1条或者多条,包含where条件的临键区间,加临键锁
2)辅助索引
等值查询
(1)命中记录,命中记录的辅助索引项+主键索引项加记录锁,辅助索引项两侧加间隙锁。
(2)未命中记录,加间隙锁范围查询
(1)没有命中任何一条记录时,加间隙锁。
(2)命中1条或者多条,包含where条件的临键区间加临键锁。命中记录的id索引项加记录锁。
7、插入意向锁(Insert Intention Locks)
在开启事务之后插入数据之前需要开启插入意向锁(把id锁上),防止其他的回话也插入相同的记录。
(1)插入意向锁是一种Gap锁,不是意向锁,在insert操作时产生。
(2)在多事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。
(3)假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之
间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。
(4)插入意向锁不会阻止任何锁,对于插入的记录会持有一个记录锁。
8、锁相关参数
Innodb所使用的行级锁定争用状态查看:
show status like 'innodb_row_lock%';
Innodb_row_lock_current_waits
:当前正在等待锁定的数量;Innodb_row_lock_time
:从系统启动到现在锁定总时间长度;Innodb_row_lock_time_avg
:每次等待所花平均时间;Innodb_row_lock_time_max
:从系统启动到现在等待最常的一次所花的时间;Innodb_row_lock_waits
:系统启动后到现在总共等待的次数;
对于这5个状态变量,比较重要的主要是:
Innodb_row_lock_time_avg
(等待平均时长)Innodb_row_lock_waits
(等待总次数)Innodb_row_lock_time
(等待总时长)这三项。
尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。
查看事务、锁的sql:
select * from information_schema.innodb_locks;
select * from information_schema.innodb_lock_waits;
select * from information_schema.innodb_trx;
六、行锁分析实战
在介绍完一些背景知识之后,接下来将选择几个有代表性的例子,来详细分析MySQL的加锁处理。当然,还是从最简单的例子说起。经常有朋友发给我一个SQL,然后问我,这个SQL加什么锁?就如同下面两条简单的SQL,他们加什么锁?
- SQL1:
select * from t1 where id = 10;
- SQL2:
delete from t1 where id = 10;
针对这个问题,该怎么回答?能想象到的一个答案是:
- SQL1:不加锁。因为MySQL是使用多版本并发控制的,读不加锁。
- SQL2:对id = 10的记录加写锁 (走主键索引)。
这个答案对吗?说不上来。即可能是正确的,也有可能是错误的,已知条件不足,这个问题没有答案。
必须还要知道以下的一些前提,前提不同,能给出的答案也就不同。要回答这个问题,还缺少哪些前提条件?
前提一: id列是不是主键?
前提二:当前系统的隔离级别是什么?
前提三:id列如果不是主键,那么id列上有索引吗?
前提四:id列上如果有二级索引,那么这个索引是唯一索引吗?
前提五:两个SQL的执行计划是什么?索引扫描?全表扫描?
没有这些前提,直接就给定一条SQL,然后问这个SQL会加什么锁,都是很业余的表现。而当这些问题有了明确的答案之后,给定的SQL会加什么锁,也就一目了然。下面,我们将这些问题的答案进行组合,然后按照从易到难的顺序,逐个分析每种组合下,对应的SQL会加哪些锁?
注:下面的这些组合,需要做一个前提假设,也就是有索引时,执行计划一定会选择使用索引进行过滤 (索引扫描)。但实际情况会复杂很多,真正的执行计划,还是需要根据MySQL输出的为准。
组合一:id列是主键,RC隔离级别
组合二:id列是二级唯一索引,RC隔离级别
组合三:id列是二级非唯一索引,RC隔离级别
组合四:id列上没有索引,RC隔离级别
组合五:id列是主键,RR隔离级别
组合六:id列是二级唯一索引,RR隔离级别
组合七:id列是二级非唯一索引,RR隔离级别
组合八:id列上没有索引,RR隔离级别
组合九:Serializable隔离级别
排列组合还没有列举完全,但是看起来,已经很多了。真的有必要这么复杂吗?事实上,要分析加锁,就是需要这么复杂。但是从另一个角度来说,只要你选定了一种组合,SQL需要加哪些锁,其实也就确定了。接下来,就让我们来逐个分析这9种组合下的SQL加锁策略。
注:在前面八种组合下,也就是RC,RR隔离级别下SQL1:select操作均不加锁,采用的是快照读,因此在下面的讨论中就忽略了,主要讨论SQL2:delete操作的加锁。
1)组合一:id主键+RC
分析
这个组合,是最简单,最容易分析的组合。id是主键
,Read Committed隔离级别
,给定SQL:delete from t1 where id = 10
; 只需要将主键上id = 10的记录加上X锁即可。如下图所示:
结论
id是主键时,此SQL只需要在id=10这条记录上加X锁即可。
2)组合二:id唯一索引+RC
分析
这个组合,id不是主键,而是一个Unique的二级索引键值
。那么在RC隔离级别
下,delete from t1 where id = 10;
需要加什么锁呢?见下图:
此组合中,id是unique索引
,而主键是name列
。此时,加锁的情况由于组合一有所不同。由于id是 unique索引
,因此delete语句
会选择走id列的索引
进行where条件的过滤
,在找到id=10
的记录后:
首先 会将unique索引
上的id=10索引
记录加上X锁,同时,会根据读取到的name列
,回主键索引(聚簇索引);
然后将聚簇索引上的name = ‘d’
对应的主键索引项加X锁。
为什么聚簇索引上的记录也要加锁?
试想一下,如果并发的一个SQL,是通过主键索引来更新:update t1 set id = 100 where name = ‘d’;
此时,如果delete语句
没有将主键索引
上的记录加锁,那么并发的update
就会感知不到delete语句
的存在,违背了同一记录上的更新/删除需要串行执行的约束。
结论
若id列是unique列,其上有unique索引。那么SQL需要加两个X锁,一个对应于id unique索引上的id = 10的记录,另一把锁对应于聚簇索引上的【name=’d’,id=10】的记录。
3)组合三:id非唯一索引+RC
分析
相对于组合一、二,组合三又发生了变化,隔离级别仍旧是RC不变,但是id列上的约束又降低了,id列不再唯一,只有一个普通的索引。假设delete from t1 where id = 10;
语句,仍旧选择id列上的索引进行
过滤where条件,那么此时会持有哪些锁?同样见下图:
根据此图,可以看到,首先,id列索引上,满足id = 10查询条件的记录,均已加锁。同时,这些记录对
应的主键索引上的记录也都加上了锁。与组合二唯一的区别在于,组合二最多只有一个满足等值查询的
记录,而组合三会将所有满足查询条件的记录都加锁。
结论:
若id列上有非唯一索引,那么对应的所有满足SQL查询条件的记录,都会被加锁。同时,这些记录在主键
索引上的记录,也会被加锁。
4)组合四:id无索引+RC
分析
相对于前面三个组合,这是一个比较特殊的情况。id列上没有索引,where id = 10; 这个过滤条件,没法通过索引进行过滤,那么只能走全表扫描做过滤。
对应于这个组合,SQL会加什么锁?或者是换句话说,全表扫描时,会加什么锁?
这个答案也有很多: 有人说会在表上加X锁;有人说会将聚簇索引上,选择出来的id = 10;的记录加上X锁。
那么实际情况呢? 请看下图:
由于id列上没有索引,因此只能走聚簇索引,进行全部扫描。从图中可以看到,满足删除条件的记录有 两条,但是,聚簇索引上所有的记录,都被加上了X锁。无论记录是否满足条件,全部被加上X锁。既不 是加表锁,也不是在满足条件的记录上加行锁。
有人可能会问?为什么不是只在满足条件的记录上加锁呢?
这是由于MySQL的实现决定的。如果一个条 件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由MySQL Server层进行 过滤。因此也就把所有的记录,都锁上了。
注:在实际的实现中,MySQL有一些改进,在MySQL Server过滤条件,发现不满足后,会调用 unlock_row方法,把不满足条件的记录放锁 (违背了2PL的约束)。这样做,保证了最后只会持有满 足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。
结论:
若id列上没有索引
,SQL会走聚簇索引
的全扫描进行过滤,由于过滤是由MySQL Server层面进行的。因此每条记录,无论是否满足条件,都会被加上X锁
。但是,为了效率考量,MySQL做了优化
,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放 锁动作不会省略。同时,优化也违背了2PL的约束。
5)组合五:id主键+RR
上面的四个组合,都是在Read Committed隔离级别
下的加锁行为,接下来的四个组合,是在 Repeatable Read隔离级别
下的加锁行为。
组合五,id列是主键列
,Repeatable Read隔离级别
,针对delete from t1 where id = 10;
这条SQL,加锁与组合一:[id主键,Read Committed] 一致。
6)组合六:id唯一索引+RR
与组合五类似,组合六的加锁,与组合二:[id唯一索引,Read Committed] 一致。两个X锁,id唯一索引满足条件的记录上一个,对应的聚簇索引上的记录一个。
7)组合七:id非唯一索引+RR
分析
还记得前面提到的MySQL的四种隔离级别的区别吗?RC隔离级别允许幻读,而RR隔离级别,不允许存在幻读。但是在组合五、组合六中,加锁行为又是与RC下的加锁行为完全一致。那么RR隔离级别下,如 何防止幻读呢?问题的答案,就在组合七中揭晓。
组合七,Repeatable Read隔离级别
,id上有一个非唯一索引
,执行delete from t1 where id = 10;
假设选择id列上的索引进行条件过滤,最后的加锁行为,是怎么样的呢?同样看下面这幅图:
此图,相对于组合三:[id列上非唯一锁,Read Committed]
看似相同,其实却有很大的区别。最大的区别在于,这幅图中多了一个GAP锁,而且GAP锁看起来也不是加在记录上的,倒像是加载两条记录之间 的位置,GAP锁有何用?
其实这个多出来的GAP锁,就是RR隔离级别,相对于RC隔离级别,不会出现幻读的关键
。确实,GAP锁锁住的位置,也不是记录本身,而是两条记录之间的GAP。所谓幻读,就是同一个事务,连续做两次当 前读 (例如:select * from t1 where id = 10 for update;
),那么这两次当前读返回的是完全相同的记录 (记录数量一致,记录本身也一致),第二次的当前读,不会比第一次返回更多的记录 (幻象)。
如何保证两次当前读返回一致的记录,那就需要在第一次当前读与第二次当前读之间,其他的事务不会 插入新的满足条件的记录并提交。为了实现这个功能,GAP锁应运而生。
如图中所示,有哪些位置可以插入新的满足条件的项 (id = 10),考虑到B+树索引的有序性,满足条件的项一定是连续存放的。记录[6,c]之前,不会插入id=10的记录;[6,c]与[10,b]间可以插入[10, aa];[10,b] 与[10,d]间,可以插入新的[10,bb],[10,c]等;[10,d]与[11,f]间可以插入满足条件的[10,e],[10,z]等;而 [11,f]之后也不会插入满足条件的记录。因此,为了保证[6,c]与[10,b]间,[10,b]与[10,d]间,[10,d]与 [11,f]不会插入新的满足条件的记录,MySQL选择了用GAP锁,将这三个GAP给锁起来。
Insert操作,如insert [10,aa],首先会定位到[6,c]与[10,b]间,然后在插入前,会检查这个GAP是否已经 被锁上,如果被锁上,则Insert不能插入记录。因此,通过第一遍的当前读,不仅将满足条件的记录锁 上 (X锁),与组合三类似。同时还是增加3把GAP锁,将可能插入满足条件记录的3个GAP给锁上,保证后 续的Insert不能插入新的id=10的记录,也就杜绝了同一事务的第二次当前读,出现幻象的情况。
有心的朋友看到这儿,可以会问:既然防止幻读,需要靠GAP锁的保护,为什么组合五、组合六,也是 RR隔离级别,却不需要加GAP锁呢?
首先,这是一个好问题。其次,回答这个问题,也很简单。GAP锁的目的,是为了防止同一事务的两次 当前读,出现幻读的情况。而组合五,id是主键;组合六,id是unique键,都能够保证唯一性。一个等值查询,最多只能返回一条记录,而且新的相同取值的记录,一定不会在新插入进来,因此也就避免了 GAP锁的使用。其实,针对此问题,还有一个更深入的问题:如果组合五、组合六下,针对SQL:select * from t1 where id = 10 for update;
第一次查询,没有找到满足查询条件的记录,那么GAP锁是否还能 够省略?此问题留给大家思考。
结论:
Repeatable Read隔离级别
下,id列上有一个非唯一索引
,对应SQL:delete from t1 where id = 10;
首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁
,加GAP上的GAP锁
,然后加主键聚簇索引
上的记录X锁
,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录X锁
,但是仍旧需要加GAP锁
,最后返回结束。
8)组合八:id无索引+RR
分析
组合八,Repeatable Read隔离级别
下的最后一种情况,id列上没有索引
。此时SQL:delete from t1 where id = 10;
没有其他的路径可以选择,只能进行全表扫描。最终的加锁情况,如下图所示:
如图,这是一个很恐怖的现象。首先,聚簇索引
上的所有记录,都被加上了X锁
。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁
。这个示例表,只有6条记录,一共需要6个记录锁,7个GAP 锁。试想,如果表上有1000万条记录呢?
在这种情况下,这个表上,除了不加锁的快照度,其他任何加锁的并发SQL,均不能执行,不能更新, 不能删除,不能插入,全表被锁死。
当然,跟组合四:[id无索引, Read Committed]
类似,这个情况下,MySQL也做了一些优化,就是所谓的semi-consistent read
。semi-consistent read开启
的情况下,对于不满足查询条件的记录,MySQL 会提前放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]之外,所有的记录锁都会被释放,同时 不加GAP锁。semi-consistent read
如何触发:要么是read committed隔离级别
;要么是Repeatable Read隔离级别
,同时设置了 innodb_locks_unsafe_for_binlog
参数。
结论:
在Repeatable Read隔离级别
下,如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发 更新/删除/插入 操作。当然,也可以通过触发semi- consistent read
,来缓解加锁开销与并发影响,但是semi-consistent read
本身也会带来其他问 题,不建议使用。
9)组合九:Serializable
分析
针对前面提到的简单的SQL,最后一个情况:Serializable隔离级别
。对于SQL2来说,Serializable隔离
级别与Repeatable Read隔离级别
完全一致,因此不做介绍。
delete from t1 where id = 10
Serializable隔离级别,影响的是SQL1这条SQL
select * from t1 where id = 10
在RC,RR隔离级别
下,都是快照读,不加锁。但是在Serializable隔离级别
,SQL1会加读锁,也就是说 快照读不复存在,MVCC并发控制降级为Lock-Based CC。
结论:
在MySQL/InnoDB
中,所谓的读不加锁,并不适用于所有的情况,而是隔离级别相关的。Serializable 隔离级别
,读不加锁就不再成立,所有的读操作,都是当前读。
七、一条复杂SQL的加锁分析
分析
写到这里,其实MySQL的加锁实现也已经介绍的八八九九。只要将本文上面的分析思路,大部分的 SQL,都能分析出其会加哪些锁。而这里,再来看一个稍微复杂点的SQL,用于说明MySQL加锁的另外 一个逻辑。SQL用例如下:
如图中的SQL,会加什么锁?假定在Repeatable Read隔离级别下 (Read Committed隔离级别下的加锁 情况,留给学生们分析),同时,假设SQL走的是idx_t1_pu索引。
在详细分析这条SQL的加锁情况前,还需要有一个知识储备,那就是一个SQL中的where条件如何拆分? 在这里,我直接给出分析后的结果:
Index key:
pubtime > 1 and puptime < 20。此条件,用于确定SQL在idx_t1_pu索引上的查询范围。Index Filter:
userid = ‘hdc’ 。此条件,可以在idx_t1_pu索引
上进行过滤,但不属于Index Key
。Table Filter:
comment is not NULL。此条件,在idx_t1_pu索引
上无法过滤,只能在聚簇索引上过滤。
在分析出SQL where条件的构成之后,再来看看这条SQL的加锁情况 (RR隔离级别),如下图所示:
从图中可以看出,在Repeatable Read隔离级别下,由Index Key所确定的范围,被加上了GAP锁; Index Filter锁给定的条件 **(userid = ‘hdc’)**何时过滤,视MySQL的版本而定,在MySQL 5.6版本之前,不 支持Index Condition Pushdown(ICP),因此Index Filter在MySQL Server层过滤,在5.6后支持了Index Condition Pushdown,则在index上过滤。若不支持ICP,不满足Index Filter的记录,也需要加上记录X 锁;若支持ICP,则不满足Index Filter的记录,无需加记录X锁 (图中,用红色箭头标出的X锁,是否要加,视是否支持ICP而定);而Table Filter对应的过滤条件,则在聚簇索引中读取后,在MySQL Server层 面过滤,因此聚簇索引上也需要X锁。最后,选取出了一条满足条件的记录[8,hdc,d,5,good],但是加锁的数量,要远远大于满足条件的记录数量。
结论:
在Repeatable Read隔离级别下,针对一个复杂的SQL,首先需要提取其where条件。
- Index Key确定的范围,需要加上GAP锁;
- Index Filter过滤条件,视MySQL版本是否支持ICP,若支持ICP,则不满足Index Filter的记 录,不加X锁,否则需要X锁;
- Table Filter过滤条件,无论是否满足,都需要加X锁。
八、死锁原理与分析
本文前面的部分,基本上已经涵盖了MySQL/InnoDB所有的加锁规则。深入理解MySQL如何加锁,有两个比较重要的作用:
- 可以根据MySQL的加锁规则,写出不会发生死锁的SQL;
- 可以根据MySQL的加锁规则,定位出线上产生死锁的原因;
下面,来看看两个死锁的例子 (一个是两个Session的两条SQL产生死锁;另一个是两个Session的一条 SQL,产生死锁):
上面的两个死锁用例。第一个非常好理解,也是最常见的死锁,每个事务执行两条SQL,分别持有了一 把锁,然后加另一把锁,产生死锁。
第二个用例,虽然每个Session都只有一条语句,仍旧会产生死锁。要分析这个死锁,首先必须用到本文 前面提到的MySQL加锁的规则。针对Session 1,从name索引出发,读到的[hdc, 1],[hdc, 6]均满足条 件,不仅会加name索引上的记录X锁,而且会加聚簇索引上的记录X锁,加锁顺序为先[1,hdc,100],后 [6,hdc,10]。而Session 2,从pubtime索引出发,[10,6],[100,1]均满足过滤条件,同样也会加聚簇索引 上的记录X锁,加锁顺序为[6,hdc,10],后[1,hdc,100]。发现没有,跟Session 1的加锁顺序正好相反, 如果两个Session恰好都持有了第一把锁,请求加第二把锁,死锁就发生了。
结论:
Whether a deadlock occurs or not does not depend on how many SQL statements there are in the transaction. [The key to the deadlock is]: the [locking order] of two (or more) Sessions is inconsistent. Using the method mentioned above in this article, analyze the locking rules of each SQL statement in MySQL, analyze the locking order of each statement, and then check whether there are locks in the opposite order between multiple concurrent SQLs, you can analyze Various potential deadlock situations can be identified, and the causes of online deadlocks can also be analyzed.
How to avoid deadlock?
By default, MySQL will actively detect deadlocks and roll back a transaction with minimal impact. Wait for the execution of another transaction to complete, and then re-execute the transaction.
How to avoid deadlock
1. Pay attention to the logic of the program
The fundamental reason is the order of program logic. The most common one is cross update
Transaction 1: Update table A -> Update table B
Transaction 2: Update table B -> Update table A
Transaction obtains two resources
2. Keep transactions lightweight
The lighter the transaction, the fewer lock resources it occupies, so the chance of deadlock is smaller.
3. Improve running speed
Avoid using subqueries, try to use primary keys, etc.
4. Submit transactions as quickly as possible to reduce the time of holding locks
The sooner the transaction is committed, the sooner the lock is released
Nine excellent links
1. How to get MySQL locks (global locks, table-level locks, row-level locks)
2. MySQL database: lock mechanism
3. Summary of common interview questions in MySQL database