After a week of liver work, I completely understand the MySQL lock

Recently, a colleague encountered a MySQL deadlock problem in production, so after helping to solve the problem, he specially spent a week sorting out all the MySQL locks. Today, let's talk about MySQL locks together.

Statement: This article is based on MySQL 8.0.30 version, InnoDB engine

The original intention of MySQL database lock design is to deal with concurrency issues and ensure data security. MySQL database locks can be divided from the following three dimensions:

  • According to the use of locks, MySQL locks can be divided into shared locks and exclusive locks;
  • According to the scope of locking, MySQL locks can be roughly divided into three categories: global locks, table-level locks and row locks;
  • From an ideological point of view, MySQL locks can be divided into two types: pessimistic locks and optimistic locks;

We will first explain shared locks and exclusive locks, and then explain global locks, table-level locks, and row locks, because some of these three types of locks are shared locks, and some are exclusive locks. Finally, we will explain pessimistic locks and Optimistic locking.
img.png

shared lock & exclusive lock

1. Shared lock

Shared lock, Share lock, also called read lock. It means that when the object is locked, other transactions are allowed to read the object, and other transactions are allowed to acquire the shared lock again from the object, but they cannot write to the object.
The locking method is:

# 方式1
select ... lock in share mode;
# 方式2
select ... for share;

If transaction T1 holds a shared (S) lock on an object, and transaction T2 needs to acquire the lock of the object again, the following two situations will occur:

  • If T2 acquires a shared (S) lock on the object, it can acquire the lock immediately;
  • If T2 acquires an exclusive (X) lock on the object, it cannot acquire the lock;

In order to better understand the above two situations, you can refer to the following execution sequence flow and example diagram:

Add a shared lock to the user table

Lock thread sessionA Thread B session B
#Open transaction
begin;
#Add a shared lock to the entire user table
select * from user lock in share mode;
#Get the shared lock on the user table ok, the select operation is successfully executed
select * from user;
#Failed to obtain the exclusive lock on the user table, the operation was blocked
delete from user where id = 1;
#Commit transaction#
The shared lock on the user table is released
commit;
#Get the exclusive lock on the user table successfully, and the delete operation executes ok
delete from user where id = 1;

img.png

Add a shared lock to the row with id=3 in the user table

Lock thread sessionA Thread B session B threadC sessionC
#Open transaction
begin;
#Add a shared lock to the row with id=3 in the user table
select * from user
where id = 3 lock in share mode;
#Get the shared lock on the user table id=3 row ok
#The select operation is executed successfully
select * from user where id=3;
#Get the shared lock on the user table id=3 row ok
#The select operation is executed successfully
select * from user where id=3;
#Get the exclusive lock on the user table id=3 line failed
#delete operation is blocked
delete from user where id = 3;
#Get the exclusive lock on the user table id=4 line successfully
#delete operation is executed successfully
delete from user where id = 4;
#Submit transaction#
The shared lock on the row of user table id=3 is released
commit;
#Get the exclusive lock on the user table id=3 row successfully
#The blocked delete operation executes ok
delete from user where id = 3;

[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-Zg3nYfxW-1665050356867)(https://yuanjava.cn/assets/md/mysql/share-lock-row .png)]

From the above two examples it can be seen that:

  • When the shared lock is added to the user table, other transactions can acquire the shared lock of the user table again, and other transactions fail to acquire the exclusive lock of the user table again, and the operation is blocked;
  • When the shared lock is added to the row with id=3 in the user table, other transactions can acquire the shared lock on the row with id=3 in the user table again, and other transactions fail to acquire the exclusive lock on the row with id=3 in the user table again, and the operation is blocked , but the transaction can acquire the exclusive lock on the user table id!=3 row again;

2. Exclusive lock

Exclusive lock, Exclusive Lock, also called write lock or exclusive lock, mainly prevents other transactions from locking the same object as the current locking transaction. The same object has two main meanings:

  • When an exclusive lock is added to the table, other transactions cannot perform update operations such as insert, update, delete, alter, drop, etc. on the table;
  • When an exclusive lock is added to a table row, other transactions cannot perform insert, update, delete, alter, drop, etc. update operations on the row;

The exclusive lock locking method is:

select ... for update;

In order to better illustrate the exclusive lock, you can refer to the following execution sequence flow and instance diagram:

Add an exclusive lock to the user table object

Lock thread sessionA Thread B session B
#Open transaction begin;
#Add an exclusive lock to the entire table of user
select * from user for update;
#Get the shared lock on the user table ok, select is executed successfully
select * from user;
#Failed to obtain the exclusive lock on the user table, the operation was blocked
delete from user where id=3;
#Commit transaction#
The exclusive release on the user table
commit;
#获取user表上的排他锁成功,操作执行ok
delete from user where id = 1;

img.png

给user表id=3的行对象加排他锁

加锁线程 sessionA 线程B sessionB 线程C sessionC
#开启事务
begin;
#给user表id=3的行加排他锁
select * from user
where id = 3 for update;
#获取user表id=3行上的共享锁ok
select * from user where id=3;
#获取user表id=3行上的共享锁ok
select * from user where id=3;
#获取user表id=3行上的排它锁失败
delete from user where id = 3;
#获取user表id=4行上的排它锁成功
delete from user where id = 4;
#提交事务
#user表id=3的行上排他锁被释放
commit;
#获取user表id=3行上的排它锁成功
#被堵塞的delete操作执行ok
delete from user where id = 3;

img.png

全局锁&表级锁&行锁

1. 全局锁

1.1 定义

全局锁,顾名思义,就是对整个数据库实例加锁。它是粒度最大的锁。

1.2 加锁

在MySQL中,通过执行 flush tables with read lock 指令加全局锁:

flush tables with read lock

指令执行完,整个数据库就处于只读状态了,其他线程执行以下操作,都会被阻塞:

  • 数据更新语句被阻塞,包括 insert, update, delete语句;
  • 数据定义语句被阻塞,包括建表 create table,alter table、drop table 语句;
  • 更新操作事务commit语句被阻塞;

1.3 释放锁

MySQl释放锁有2种方式:

  1. 执行 unlock tables 指令
unlock tables
  1. 加锁的会话断开,全局锁也会被自动释放

为了更好的说明全局锁,可以参照下面的执行顺序流和实例图:

加锁线程 sessionA 线程B sessionB
flush tables with read lock; 加全局锁
select user表ok select user表ok
insert user表堵塞 insert user表堵塞
delete user表堵塞 delete user表堵塞
drop user 表堵塞 drop user 表堵塞
alter user表 堵塞 alter user表 堵塞
unlock tables; 解锁
被堵塞的修改操作执行ok 被堵塞的修改操作执行ok

img.png

通过上述的实例可以看出,当加全局锁时,库下面所有的表都处于只能状态,不管是当前事务还是其他事务,对于库下面所有的表只能读,不能执行insert,update,delete,alter,drop等更新操作。

1.4 使用场景

全局锁的典型使用场景是做全库逻辑备份,在备份过程中整个库完全处于只读状态。如下图:

img.png

  • 假如在主库上备份,备份期间,业务服务器不能对数据库执行更新操作,因此涉及到更新操作的业务就瘫痪了;
  • 假如在从库上备份,备份期间,从库不能执行主库同步过来的 binlog,会导致主从延迟越来越大,如果做了读写分离,那么从库上获取数据就会出现延时,影响业务;

从上述分析可以看出,使用全局锁进行数据备份,不管是在主库还是在从库上进行备份操作,对业务总是不太友好。那不加锁行不行?我们可以通过下面还钱转账的例子,看看不加锁会不会出现问题:

img.png

  • 备份前:账户A 有1000,账户B 有500
  • 此时,发起逻辑备份
  • 假如数据备份时不加锁,此时,客户端A 发起一个还钱转账的操作:账户A 往账户B 转200
  • 当账户A 转出200完成,账户B 转入200 还未完成时,整个数据备份完成
  • 如果用该备份数据做恢复,会发现账户A 转出了200,账户B 却没有对应的转入记录,这样就会产生纠纷:A 说我账户少了 200, B 说我没有收到,最后,A,B谁都不干。

既然不加锁会产生错误,加全局锁又会影响业务,那么有没有两全其美的方式呢?

有,MySQL官方自带的逻辑备份工具 mysqldump,具体指令如下:

mysqldump –single-transaction

执行该指令,在备份数据之前会先启动一个事务,来确保拿到一致性视图, 加上 MVCC 的支持,保证备份过程中数据是可以正常更新。但是,single-transaction方法只适用于库中所有表都使用了事务引擎,如果有表使用了不支持事务的引擎,备份就只能用 FTWRL 方法。

2. 表级锁

MySQL 表级锁有两种:

  1. 表锁
  2. 元数据锁(metadata lock,MDL)

2.1 表锁

表锁就是对整张表加锁,包含读锁和写锁,由MySQL Server实现,表锁需要显示加锁或释放锁,具体指令如下:

# 给表加写锁
lock tables tablename write;

# 给表加读锁
lock tables tablename read;

# 释放锁
unlock tables;

读锁:代表当前表为只读状态,读锁是一种共享锁。需要注意的是,读锁除了会限制其它线程的操作外,也会限制加锁线程的行为,具体限制如下:

  1. 加锁线程只能对当前表进行读操作,不能对当前表进行更新操作,不能对其它表进行所有操作;
  2. 其它线程只能对当前表进行读操作,不能对当前表进行更新操作,可以对其它表进行所有操作;

为了更好的说明读锁,可以参照下面的执行顺序流和实例图:

加锁线程 sessionA 线程B sessionB
#给user表加读锁
lock tables user read;
select user表 ok select user表 ok
insert user表被拒绝 insert user表堵塞
insert address表被拒绝 insert address表ok
select address表被拒绝 alter user表堵塞
unlock tables; 释放锁
被堵塞的修改操作执行ok

img.png

写锁:写锁是一种独占锁,需要注意的是,写锁除了会限制其它线程的操作外,也会限制加锁线程的行为,具体限制如下:

  1. 加锁线程对当前表能进行所有操作,不能对其它表进行任何操作;
  2. 其它线程不能对当前表进行任何操作,可以对其它表进行任何操作;

为了更好的说明写锁,可以参照下面的执行顺序流和实例图:

加锁线程 sessionA 线程B sessionB
#给user表加写锁
lock tables user write;
select user表 ok select user表 ok
insert user表被拒绝 insert user表堵塞
insert address表被拒绝 insert address表ok
select address表被拒绝 alter user表堵塞
unlock tables; 释放锁
堵塞在user表的上更新操作执行ok

img.png

2.2 MDL元数据锁

元数据锁:metadata lock,简称MDL,它是在MySQL 5.5版本引进的。元数据锁不用像表锁那样显式的加锁和释放锁,而是在访问表时被自动加上,以保证读写的正确性。加锁和释放锁规则如下:

  • MDL读锁之间不互斥,也就是说,允许多个线程同时对加了 MDL读锁的表进行CRUD(增删改查)操作;
  • MDL写锁,它和读锁、写锁都是互斥的,目的是用来保证变更表结构操作的安全性。也就是说,当对表结构进行变更时,会被默认加 MDL写锁,因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
  • MDL读写锁是在事务commit之后才会被释放;

为了更好的说明 MDL读锁规则,可以参照下面的顺序执行流和实例图:

加锁线程 sessionA 其它线程 sessionB
开启事务
begin;
select user表,user表会默认加上MDL读锁
select user表ok select user表ok
insert user表ok insert user表ok
update user表ok update user表ok
delete user表ok delete user表ok
alter user表,获取MDL写锁失败,操作被堵塞
commit;提交事务,MDL读锁被释放
被堵塞的修改操作执行ok

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sIyVqMzT-1665050356868)(https://yuanjava.cn/assets/md/mysql/MDL-read-lock.png)]

为了更好的说明 MDL写锁规则,可以参照下面的顺序执行流和实例图:

加锁线程 sessionA 线程B sessionB 线程C sessionC
#开启事务
begin;
#user表会默认加上MDL读锁
select user表,
select user表ok select user表ok select user表ok
#获取MDL写锁失败
alter user表操作被堵塞
#获取MDL读锁失败
select * from user;
提交事务,MDL读锁被释放
#MDL写锁被释放
被堵塞的alter user操作执行ok
#被堵塞的select 操作执行ok

img.png

2.3 意向锁

由于InnoDB引擎支持多粒度锁定,允许行锁和表锁共存,为了快速的判断表中是否存在行锁,InnoDB推出了意向锁。

意向锁,Intention lock,它是一种表锁,用来标识事务打算在表中的行上获取什么类型的锁。
不同的事务可以在同一张表上获取不同种类的意向锁,但是第一个获取表上意向排他(IX) 锁的事务会阻止其它事务获取该表上的任何 S锁 或 X 锁。反之,第一个获得表上意向共享锁(IS) 的事务可防止其它事务获取该表上的任何 X 锁。

意向锁通常有两种类型:

  • 意向共享锁(IS),表示事务打算在表中的各个行上设置共享锁。
  • 意向排他锁(IX),表示事务打算对表中的各个行设置排他锁。

意向锁是InnoDB自动加上的,加锁时遵从下面两个协议:

  • 事务在获取表中行的共享锁之前,必须先获取表上的IS锁或更强的锁。
  • 事务在获取表中行的排他锁之前,必须先获取表上的IX锁。

为了更好的说明意向共享锁,可以参照下面的顺序执行流和实例图:

加锁线程 sessionA 线程B sessionB
#开启事务
begin;
#user表id=6加共享行锁 ,默认user表会 加上IS锁
select * from user where id = 6 for share;
# 观察IS锁
select * from performance_schema.data_locks\G

img.png

加锁线程 sessionA 线程B sessionB
#开启事务
begin;
#user表id=6加排他锁,默认user表会 加上IX锁
select * from user where id = 6 for update;
# 观察IX锁
select * from performance_schema.data_locks\G

img.png

2.4 AUTO-INC锁

AUTO-INC锁是一种特殊的表级锁,当表中有AUTO_INCREMENT的列时,如果向这张表插入数据时,InnoDB会先获取这张表的AUTO-INC锁,等插入语句执行完成后,AUTO-INC锁会被释放。

AUTO-INC锁可以使用innodb_autoinc_lock_mode变量来配置自增锁的算法,innodb_autoinc_lock_mode变量可以选择三种值如下表:

innodb_autoinc_lock_mode 含义
0 传统锁模式,采用 AUTO-INC 锁
1 连续锁模式,采用轻量级锁
2 交错锁模式(MySQL8默认),AUTO-INC和轻量级锁之间灵活切换

为了更好的说明意AUTO-INC锁,可以参照下面的顺序执行流和实例图:

2.5 锁的兼容性

下面的图表总结了表级锁类型的兼容性

X IX S IS
X 冲突 冲突 冲突 冲突
IX 冲突 兼容 冲突 兼容
S 冲突 冲突 兼容 兼容
IS 冲突 兼容 兼容 兼容

3. 行锁

行锁是针对数据表中行记录的锁。MySQL 的行锁是在引擎层实现的,并不是所有的引擎都支持行锁,比如,InnoDB引擎支持行锁而 MyISAM引擎不支持。

InnoDB 引擎的行锁主要有三类:

  1. Record Lock: 记录锁,是在索引记录上加锁;
  2. Gap Lock:间隙锁,锁定一个范围,但不包含记录;
  3. Next-key Lock:Gap Lock + Record Lock,锁定一个范围(Gap Lock实现),并且锁定记录本身(Record Lock实现);

3.1 Record Lock

Record Lock:记录锁,是针对索引记录的锁,锁定的总是索引记录。

例如,select id from user where id = 1 for update; for update 就显式在索引id上加行锁(排他锁),防止其它任何事务 update或delete id=1 的行,但是对user表的insert、alter、drop操作还是可以正常执行。

为了更好的说明 Record Lock锁,可以参照下面的执行顺序流和实例图:

加锁线程 sessionA 线程B sessionB 线程B sessionC
#开启事务
begin;
给user表id=1加写锁
select id from user
where id = 1 for update;
update user set name = ‘name121’
where id = 1;
查看 InnoDB监视器中记录锁数据
show engine innodb status\G
commit提交事务
record lock 被释放
被堵塞的update操作执行ok

img.png

3.2 Gap Lock

Gap Lock:间隙锁,锁住两个索引记录之间的间隙上,由InnoDB隐式添加。比如(1,3) 表示锁住记录1和记录3之间的间隙,这样记录2就无法插入,间隙可能跨越单个索引值、多个索引值,甚至是空。

img.png

为了更好的说明 Gap Lock间隙锁,可以参照下面的顺序执行流和实例图:

加锁线程 sessionA 线程B sessionB 线程C sessionC
#开启事务
begin;
加锁
select * from user
where age = 10 for share;
insert into user(id,age) values(2,20);
#查看 InnoDB监视器中记录锁数据
show engine innodb status\G
commit提交事务
Gap Lock被释放
被堵塞的insert操作执行ok

img.png

上图中,事务A(sessionA)在加共享锁的时候产生了间隙锁(Gap Lock),事务B(sessionB)对间隙中进行insert/update操作,需要先获取排他锁(X),导致阻塞。事务C(sessionC)通过"show engine innodb status\G" 指令可以查看到间隙锁的存在。需要说明的,间隙锁只是锁住间隙内部的范围,在间隙外的insert/update操作不会受影响。

Gap Lock锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。

3.3 Next-Key Lock

Next-Key锁,称为临键锁,它是Record Lock + Gap Lock的组合,用来锁定一个范围,并且锁定记录本身锁,它是一种左开右闭的范围,可以用符号表示为:(a,b]。

img.png

为了更好的说明 Next-Key Lock间隙锁,可以参照下面的顺序执行流和实例图:

加锁线程 sessionA 线程B sessionB 线程C sessionC 线程D sessionD
#开启事务
begin;
加锁
select * from user
where age = 10 for share;
#获取锁失败,insert操作被堵塞
insert into user(id,age)
values(2,20);
update user set name=‘name1’
where age = 10;
#查看 InnoDB监视器中记录锁数据
show engine innodb status\G
提交事务Gap Lock被释放
commit
被堵塞的insert操作执行ok 被堵塞的update操作执行ok

img.png

上图中,事务A(sessionA)在加共享锁的时候产生了间隙锁(Gap Lock),事务B(sessionB)对间隙中进行insert操作,需要先获取排他锁(X),导致阻塞。
事务C(sessionC)对间隙中进行update操作,需要先获取排他锁(X),导致阻塞。
事务D(sessionD)通过"show engine innodb status\G" 指令可以查看到间隙锁的存在。需要说明的,间隙锁只是锁住间隙内部的范围,在间隙外的insert/update操作不会受影响。

3.4 Insert Intention Lock

插入意向锁,它是一种特殊的间隙锁,特指插入操作产生的间隙锁。

为了更好的说明 Insert Intention Lock锁,可以参照下面的顺序执行流和实例图:

加锁线程 sessionA 线程B sessionB 线程C sessionC
#开启事务
begin;
加锁
select * from user
where age = 10 for share;
#获取锁失败,insert操作被堵塞
insert into user(id,age) values(2,20);
#查看 InnoDB监视器中记录锁数据
show engine innodb status\G
commit提交事务
Gap Lock被释放
#被堵塞的insert操作执行ok
insert into user(id,age) values(2,20);

img.png

乐观锁&悲观锁

In MySQL, whether it is pessimistic locking or optimistic locking, it is a kind of abstraction of people's thinking on concepts, and they themselves are implemented by using the locking mechanism provided by MySQL. In fact, in addition to MySQL data, there are also concepts of optimistic locking and pessimistic locking in Java languages.

  • Pessimistic locking can be understood as: before modifying any record, first try to add an exclusive lock to the record (exclusive locking), adopting the strategy of acquiring the lock first and then operating the data, which may cause deadlock;
  • Optimistic locking, compared to pessimistic locking, generally does not use the locking mechanism of the database, but uses operations such as version number comparison, so optimistic locking will not cause deadlock problems;

Deadlocks and Deadlock Detection

When different threads in a concurrent system have cyclic resource dependencies, and the threads involved are all waiting for other threads to release resources, it will cause these threads to enter an infinite waiting state, which is called deadlock. You can check the deadlock with the following command

show engine innodb status\G

When a deadlock occurs, there are two strategies:

  • One strategy is to just go ahead and wait until it times out. This timeout can be set by the parameter innodb_lock_wait_timeout, and the default value of innodb_lock_wait_timeout in InnoDB is 50s.
  • Another strategy is to initiate deadlock detection, and after a deadlock is found, actively roll back a transaction in the deadlock chain so that other transactions can continue to execute. Set the parameter innodb_deadlock_detect to on to enable deadlock detection.

Summarize

This article explains the locks in MySQL based on the MySQL 8.0.30 version and the InnoDB engine. Each lock has its specific usage scenarios.
As a Java programmer who often deals with MySQL, the deeper the understanding of MySQL locks, the better it can help us write high-performance SQL statements.

Guess you like

Origin blog.csdn.net/m0_54369189/article/details/127185867