MySQL锁(读锁、共享锁、写锁、S锁、排它锁、独占锁、X锁、表锁、意向锁、自增锁、MDL锁、RL锁、GL锁、NKL锁、插入意向锁、间隙锁、页锁、悲观锁、乐观锁

2024年 3月 10日 35.6k 0

本文说明的是MySQL锁,和操作系统或者编程语言的锁无关。

概念

作用:在并发情况下让数据正确的读写。
优点:并发情况下对数据读写可控,防止出错。
缺点:降低性能、增加难度。

分类

  • 数据操作类型划分
    • 读锁(共享锁、S锁)
    • 写锁(排它锁、独占锁、X锁)
  • 粒度划分
    • 表级锁
      • S锁、X锁
      • 意向锁
      • 自增锁
      • 元数据锁
    • 行级锁
      • 记录锁
      • 间隙锁
      • 临键锁
      • 插入意向锁
    • 页级锁
  • 严格度划分
    • 悲观锁
    • 乐观锁
  • 加锁方式
    • 隐式锁
    • 显示锁
  • 其它
    • 全局锁
    • 死锁

测试用表

CREATE TABLE `cs` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `num1` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '数字列1',
  `num2` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '数字列2',
  `s1` varchar(2000) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字符串列1',
  `s2` varchar(2000) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字符串列2',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `temp`.`cs` (`id`, `num1`, `num2`, `s1`, `s2`) VALUES (1, 1, 1, 'abc', 'xyz');
INSERT INTO `temp`.`cs` (`id`, `num1`, `num2`, `s1`, `s2`) VALUES (2, 2, 2, 'ABC', 'XYZ');

读锁、共享锁、S锁

读锁、共享锁、S锁(Share Lock)是一个东西。
锁的是:允许同时有多个事务,对数据只能读,不能写。
写法:

select ... lock in share mode;
select ... for share; #MySQL8

测试:

步骤 会话A 会话B 备注
1 start transaction; start transaction; 双方开启事务
2 select * from cs where id = 1 lock in share mode; select * from cs where id = 1 lock in share mode; 双方添加共享锁,都能成功添加
3 commit; commit; 正常提交事务,无报错

写锁、排它锁、X锁、独占锁

写锁、排它锁、X锁(Exclusive Lock)、独占锁是同一个东西。
锁的是:仅允许同时有1个锁独占该事务,具有排它性,不允许其它任何类型的锁再占用该事务。

select ... for update;

测试:
X锁排斥S锁1

步骤 会话A 会话B 备注
1 start transaction; start transaction; 双方开启事务
2 select * from cs where id = 1 lock in share mode; select * from cs where id = 1 for update; 会话A添加S锁,会话B添加X锁
3 / 阻塞 会话B添加X锁被阻塞,说明X锁有排它性
4 commit; / 会话B X锁添加成功
5 / commit; 会话B提交事务,结束流程

X锁排斥S锁2

步骤 会话A 会话B 备注
1 start transaction; start transaction; 双方开启事务
2 select * from cs where id = 1 for update; select * from cs where id = 1 lock in share mode; 会话A添加X锁,会话B添加S锁
3 / 阻塞 会话B添加S锁被阻塞,说明X锁有排它性
4 commit; / 会话B S锁添加成功
5 / commit; 会话B提交事务,结束流程

X锁排斥X锁

步骤 会话A 会话B 备注
1 start transaction; start transaction; 双方开启事务
2 select * from cs where id = 1 for update; select * from cs where id = 1 for update; 双方添加X锁
3 / 阻塞 会话B添加X锁阻塞,证明排它性
4 commit; / 会话B X锁添加成功
5 / commit; 会话B提交事务,结束流程

S锁、X锁常见误区

注意:在同一个事务里面,行级X锁或S锁,允许其它SQL语句对此行的写操作。
如下,每条SQL都能成功执行。
所以说锁,锁的是对外的事务,对内(当前会话)不做限制。

start transaction;
select * from cs where id = 1 for update;
update cs set num1 = 2 where id = 1;
commit;

start transaction;
select * from cs where id = 1 lock in share mode;
update cs set num1 = 2 where id = 1;
commit;

注意:在任意个事务里面,可以对同一条不存在的数据共同添加X和S锁,不会阻塞。

事务A
start transaction;
select * from cs where id = 1234 for update;
select * from cs where id = 1234 lock in share mode;

事务B
start transaction;
select * from cs where id = 1234 for update;
select * from cs where id = 1234 lock in share mode;

事务A
commit;

事务B
commit;

注意:事务A加了X锁,不影响事务B读这条数据。

事务A
start transaction;
select * from cs where id = 1 for update; #能正常读出数据

事务B
start transaction;
select * from cs where id = 1;

事务A
commit;

事务B
commit;

对阻塞超时时间的优化

遇到X锁会阻塞,默认阻塞50秒,50秒过后阻塞停止,但不会自动的回滚或者提交事务。MySQL提供了一些阻塞时间的优化方向
方式1:配置优化

查看阻塞时间,单位秒
show variables like 'innodb_lock_wait_timeout';
或
select @@innodb_lock_wait_timeout;

修改
set session innodb_lock_wait_timeout = 10; #当前会话生效
或
set global innodb_lock_wait_timeout = 10; #全局生效,重启后新配置丢失
注意global的修改对当前线程是不生效的,只有建立新的连接才生效

或者修改配置文件
vim /etc/my.cnf
[mysqld]
innodb_lock_wait_timeout = 10

方式2:SQL语句层面,MySQL8新特性
select ... nowait:会立即停止,但报错。
select ...skip locked:会立即停止,但不会报错,结果中不包含被锁定的行。

表锁、页锁、行锁

这是锁定的粒度,3个依次递减,粒度越小越好。力度小意味着更少的资源被锁定,可以提高并发性。

表锁

表锁就是锁住了整张表,所以表锁比行锁,更不容易发生死锁的情况。
注意:MyISAM引擎使用select,会给当前表加上S锁,在写操作时会加上X锁。

查看一些数据库的表,都加了那些锁
show open tables;
In_use: 是否正在被使用,如果为 1 则表示该表正在被某个线程使用。
Name_locked: 是否被上了表级锁,如果为 1 则表示该表被锁定。
+--------------------+------------------------------------------------------+--------+-------------+
| Database           | Table                                                | In_use | Name_locked |
+--------------------+------------------------------------------------------+--------+-------------+
| performance_schema | events_waits_summary_by_user_by_event_name           |      0 |           0 |
| performance_schema | events_waits_summary_global_by_event_name            |      0 |           0 |
+--------------------+------------------------------------------------------+--------+-------------+


为某个表添加X锁
lock table 表名 write;

为某个表添加S锁
lock table 表名 read;

取消表锁,部分XS
unlock tables;

表锁权限如下(MySQL5.7)

表锁类型 当前会话对当前表可读 当前会话对当前表可写 其它会话对当前表可读 其它会话对当前表可写
S锁 阻塞
X锁 阻塞 阻塞 阻塞

表级锁->意向锁、意向共享锁、意向排它锁、IS锁、IX锁

意向锁(Intention Shared Lock)是自动维护的锁,用于表级别上表明事务对表中某个行的操作意向,意思是在表级别上声明了某个行已经加锁,优化事务之间的并发访问。意向锁分为两种:意向共享锁(IS)和意向排它锁(IX)。
MyISAM引擎不支持,InnoDB引擎支持。
MySQL添加行级锁,都会向上声明添加了意向锁。
注意意向排它锁与意向排它锁不冲突,意向排它锁与意向共享锁都不冲突,如果冲突了,锁的灵活性将大大降低。距离说明:两个不同的行X锁各生成一个IX锁,如果IX锁冲突,则无法创建两个行X锁,这种巨大的bug不允许出现。

优点:

  • 提高并发性:如果没有意向锁,在一个百万级的大表中加一个锁,可能需要逐行遍历,看看有没有加其它锁,会影响当前添加的锁,但是有了意向锁,在表的层面就可以获取先前的锁,提高性能。
  • 避免死锁:通过意向锁,可以快速判断哪些锁请求是互斥的,减少锁任务堆积引起的复杂度增加,复杂度一上来,就容易有死锁。

演示:行X锁与表S锁

步骤 会话A 会话B 说明
1 start transaction; / 会话A开启事务
2 select * from cs where id = 1 for update; / 会话A加一个行X锁,相当于自动添加了一个IX锁
3 / lock table cs read; 会话B添加表S锁
4 / 阻塞 这个过程就是表锁检测到了事先添加的IX锁,行级X锁排斥表级S锁,阻塞了
5 commit; 成功加上了表级S锁 会话A事务提交,锁资源释放
6 / unlock tables; 释放表级S锁

表级锁->自增锁、AI锁

自增锁(Auto Increment Lock)是指对自增长列(一般是主键)确保唯一性的一种锁机,如果没有自增锁,多个事务并发的执行,该列的值就可能一致。
为了避免重复,自增锁可能会导致插入操作的串行化,降低并发性能,这是它的缺点。
演示:

步骤 会话A 会话B 说明
1 start transaction; start transaction; 双方开启事务
2 INSERT INTO cs (num1, num2, s1, s2) VALUES (2, 2, 'ABC', 'XYZ'); INSERT INTO cs (num1, num2, s1, s2) VALUES (2, 2, 'ABC', 'XYZ'),(2, 2, 'ABC', 'XYZ'); 双方新增3条记录
3 commit commit; 提交事务
4 select id from cs select id from cs 并未发现报错,或者id重复的现象

表级锁->元数据锁、MDL锁

MDL(Metadata Lock)听起来高大上,实际上就是DDL操作时自动加锁,所以不需要手动处理。
当要对表做增删改查操作时,会获取一个 MDL 读锁来阻止对表结构的修改。
更改表结构时,会加MDL写锁,阻塞其它线程的读写操作,直到结构变更操作完成。
演示 MDL读锁:

步骤 会话A 会话B 说明
1 start transaction; / 会话A开启事务
2 INSERT INTO cs (num1, num2, s1, s2) VALUES (2, 2, 'ABC', 'XYZ'); alter table cs add index (num1); 会话B新增一个普通索引
3 / 阻塞 会话B被阻塞
4 commit; 索引添加成功 MDL锁释放

演示 MDL写锁:

步骤 会话A 会话B 会话C 说明
1 start transaction; start transaction; / AB方开启事务
2 INSERT INTO cs (num1, num2, s1, s2) VALUES (2, 2, 'ABC', 'XYZ'); alter table cs add index (num2); / 会话B新增一个普通索引
3 / 阻塞 / 用会话A的MDL读锁去阻塞会话B
4 / / select * from cs 用会话B的MDL写锁去阻塞会话C
5 / 阻塞 阻塞 MDL写锁阻塞了查询操作
6 rollback; rollback; 成功查询出数据 事务回滚,锁资源释放。但undo log不记录DDL语句,所以索引被添加无法回滚。

行锁->记录锁、RL锁

记录锁(Record Lock)就是普通的对某一行上的X或者S锁。
注意InnoDB引擎下,支持行锁MyISAM不支持。
测试:

步骤 会话A 会话B 备注
1 start transaction; start transaction; 双方开启事务
2 select * from cs where id = 1 lock in share mode; select * from cs where id = 1 lock in share mode; 双方添加共享锁,都能成功添加
3 commit; commit; 正常提交事务,无报错

行锁->间隙锁、GL锁

间隙锁(Gap Lock)就是在数据间隙加的锁,用于防止事务在一个范围内插入新的符合条件的行,以避免中间插入的幻读的问题(MySQL默认的RR隔离级别会产生幻读问题)。
例如一个一个不连续的id列,1,6,10,如果在1~6之间添加一个锁,就可以1~6防止幻读的插入操作,10后面间隙锁防止幻读就失效了,需要再次声明新的间隙锁。1~6之间没有具体的数据,所以X间隙锁,或者是S间隙锁,没有什么区别。

MySQL默认的RR级别会产生幻读,幻读是指在同一事务中,先后执行相同的查询范围,查询到的数量不一致(mysql的select机制测不出来,两个事务需要插入相同的id引起的报错才能测出来),在并发环境中,幻读通常是因为其他事务在同一范围内插入新数据导致的。以下的演示,不演示幻读,只演示间隙锁阻止幻读。

测试:

步骤 会话A 会话B 备注
1 start transaction; start transaction; 双方开启事务
2 select * from cs where id = 4 lock in share mode; INSERT INTO cs (id, num1, num2, s1, s2) VALUES (3, 1, 1, 'abc', 'xyz'); 会话A添加间隙锁,会话B插入数据这个区间的数据
3 / 阻塞 可见间隙锁可以阻止区间幻读
4 commit; commit; 会话A关闭事务,会话B提交插入数据

当前id最大值为10,测试大于10的幻读区间

步骤 会话A 会话B 备注
1 start transaction; start transaction; 双方开启事务
2 sselect * from cs where id = 14 lock in share mode; INSERT INTO cs (id, num1, num2, s1, s2) VALUES (10000, 1, 1, 'abc', 'xyz'); 会话A添加间隙锁,会话B插入id为10000的数据
3 / 阻塞 可见间隙锁可以防止最大记录 ~ +∞之间的幻读
4 commit; commit; 会话A关闭事务,会话B提交插入数据

当前id最大值为100,同样的方法测试小于100的幻读区间

步骤 会话A 会话B 备注
1 start transaction; start transaction; 双方开启事务
2 sselect * from cs where id = 10001 lock in share mode; INSERT INTO cs (id, num1, num2, s1, s2) VALUES (9999, 1, 1, 'abc', 'xyz'); 会话A添加间隙锁,会话B插入id为9999的数据
3 / 成功插入 间隙锁未能阻塞幻读插入
4 commit; commit; 会话A关闭事务,会话B提交插入数据

行锁->临键锁、NKL锁

临键锁(next-key lock)相当于记录锁+间隙锁,用于防止幻读的锁。
测试:现表中有id为1,6,10三条数据。

步骤 会话A 会话B 备注
1 start transaction; start transaction; 双方开启事务
2 select * from cs where id between 1 and 6 lock in share mode; INSERT INTO cs (id, num1, num2, s1, s2) VALUES (4, 1, 1, 'abc', 'xyz'); 会话A添加临键锁,会话B插入id为4的数据
3 / 阻塞 临键锁阻塞幻读插入
4 commit; commit; 双方提交事务,结束流程

行锁->插入意向锁、IIL锁

插入意向锁(Insert Intention Lock)是间隙锁的一种,由insert行插入前的操作。该锁表示插入的意图,即插入同一索引间隙的多个事务如果没有在间隙内的相同位置插入,则insert不需要相互等待。
通俗讲就是如果间隙锁得到了释放,则需要insert的多个事务,不会阻塞。
测试:现表中有id为1,6,10三条数据。

步骤 会话A 会话B 会话C 备注
1 start transaction; start transaction; start transaction; 三方开启事务
2 select * from cs where id = 5 lock in share mode; INSERT INTO cs (id, num1, num2, s1, s2) VALUES (4, 1, 1, 'abc', 'xyz'); INSERT INTO cs (id, num1, num2, s1, s2) VALUES (4, 1, 1, 'abc', 'xyz'); 事务A上间隙锁,其余事务插入数据
3 / 阻塞 阻塞 间隙锁起作用阻塞B、C会话
4 commit; 成功插入 成功插入 插入意向锁起作用,让两个插入的事务不发生阻塞
5 commit; commit; commit; 提交事务,结束流程

页锁

页锁是一种锁定级别,用于控制对数据表中的页(MySQL对表数据读写的基本单位)的访问。
由于SQL语句层面,DDL、DML、DQL,都是针对库表或者表数据的读写操作,对于页极少操作,页锁粒度较大,其它锁范围使用场景够用,所以了解概念就行。

悲观锁

之前写过详细的文章可进行参考:MySQL乐观锁与悲观锁。

悲观锁比较悲观,假设数据一定会造成冲突,属于MySQL层面的锁。通过加锁阻塞其他事务,悲观锁可以保证在任何时刻,只有一个事务能够修改或访问共享资源,从而实现了强一致性。这意味着在悲观锁机制下,每个事务的读写操作都是有序、线性的。
需要事务的参与。

悲观锁,最经典的场景就是防超卖,一共10个库存,由于并发情况下,两个请求查询到的库存都是10,一个请求下单6个,一个请求下单8个,如果不加锁使其在事务中阻塞(阻塞可以理解为强制让事务串行执行),那就会有超卖的情况发生,即使是超卖,库存也不会显示为负,因为并发情况下两个请求检测到的库存都是10,大于8和6,最终扣库存的结果不是10-8-6=-4(这是串行请求的理论值),而是2或者4。

适用场景
写多写操作的前提,是保证数据不出错,悲观锁的机制很符合。

优点

  • 强一致性:基于事务又加锁,一致性可以保证。
  • 实现简单:在事务中for update即可,开发者不需要在这上面关注太多。

缺点

  • 死锁风险:悲观锁在使用不当的情况下可能导致死锁。如果多个事务持有锁并相互等待对方释放锁的情况发生,就可能发生死锁。
  • 性能较低:悲观锁通常需要在整个事务过程中锁定资源,这可能导致其他事务阻塞。

简单测试:模拟并发情况下防超卖,假设cs表的num1字段为库存字段

步骤 会话A 会话B 备注
1 start transaction; start transaction; 双方开启事务
2 select num1 from cs where id = 1 for update;update cs set num1 = num1 - 6 where id = 1 and num1 >= 6; select num1 from cs where id = 1 for update;update cs set num1 = num1 - 8 where id = 1 and num1 >= 8; 双方添加X行锁,并准备扣减库存
3 / 阻塞 此时必有一个事务阻塞,等这个事务扣完库存提交事务后,在执行另一个扣库存的事务
4 commit; commit; 双方提交事务,双方代码执行成功,但会话B库存不足,最终剩余库存4,结束流程

注意,实际的开发逻辑,与以上示例况有偏差。
若前端检测到库存不足,直接拦截用户的下单动作并提示,就不会有后端的流程。
若前端认为库存充足,后端可能是下单时预扣库存,预扣库存的环节提前就检测当前其库存是否够当前的购买数量,若不够,则直接终止流程,返回用户结果。
若库存够用但未支付,若超时30分钟未支付则取消订单(这个延时队列可以用Redis Zset 或者RabbitMQ等其它消息中间件实现),若支付,则算一笔成功的交易。
这是MySQL能抗住的情况,如果抗不住,就需要Redis+Lua的单线程事务来阻止超卖的发生。

乐观锁

之前写过详细的文章可进行参考:MySQL乐观锁与悲观锁

乐观锁很乐观,假设数据一般情况不会造成冲突,属于程序层面的逻辑锁,在数据进行更新时,才进行锁的检测。是通过添加一个版本号的方式实现的,每当数据这一行所在的数据发生变化,则对应的版本号+1,更新数据时,将版本号作为查询条件。
至于是否要加事务,看写操作单条数据还是写操作多条数据。

注意:网上很多解决方案用时间戳来做version字段,我持反对意见,并发可能是一瞬间的事,不到一秒就有好多请求,用时间戳粒度太大,用随机字符串都比用这个强。

适用场景

  • 读多写少:由于并发写操作较少,乐观锁的修改数据受影响行数为0概率也较低。
    允许一定量的重试或不需要重试的场景:这个要根据业务,否则来回重试会降低性能。
    优点
  • 实现简单:乐观锁在代码上就可以实现,不需要额外对数据库额外操作。
  • 无死锁风险:悲观锁有死锁风险,乐观锁没有。
    无需重试情况下,性能较高:乐观锁机制在并发访问情况下,不需要像悲观锁那样阻塞其他事务,提供了更高的并发性能,前提当前业务需求能容忍写操作失败的情况。

缺点

  • 并发冲突:多加了一个where条件,只能保证数据最终不会出错,不能保证每条写操作的SQL都执行成功(也就是受影响行数>0)。
  • 不提供强一致性:强一致性要求数据的状态在任何时刻都保持一致,悲观锁是到写操作那一步才去验证,期间只是做了个where条件的过滤。
  • ABA问题:一个字段的值在请求X中查询出来是A,后续代码实现乐观锁,因为并发量大,同时过来一个Y请求,将A值改成了B,因为一些业务原因又改成了A,整个过程虽然不影响请求X的结果,且能正常执行,但是联合其它数据,这个情况是否符合业务场景,不好说,所以最好的解决方案,就是专门做一个version字段,且不会与之前的version重复,即可,把这个version字段作为where条件,而不是存A或者B字段的所在字段作为where条件。
  • 当前的请求中了上一个乐观锁的招,导致的版本号不一样,需要重试,反复的重试也降低性能,由于这个原因,所以用于读操作多的场景。

简单测试:模拟并发情况下防超卖,假设cs表的num1字段为库存字段,s1为version字段,一共10个库存,两个请求查询到的库存都是10,一个请求下单6个,一个请求下单8个。

步骤 会话A 会话B 备注
1 select * from cs where id = 1 select * from cs where id = 1 双方获取版本号和库存
2 update cs set num1 = num1 - 6,s1 = 'version2' where id = 1 and s1 = 'version1' and num1 >=6; update cs set num1 = num1 - 8,s1 = 'version1' where id = 1 and s1 = 'version1' and num1 >=8; 乐观锁无需事务,但是需要多个原子性的业务场景,仍推荐加事务
3 扣库存成功 虽然是并发场景,但A执行略快于B,此时版本号已修改为version2 会话B执行成功,但库存扣减失败
4 / select * from cs where id = 1 会话B进行重试,并重新获取版本号和库存
5 / update cs set num1 = num1 - 8,s1 = 'version3' where id = 1 and s1 = 'version2' and num1 >=8; 双方代码执行成功,但会话B库存不足,最终剩余库存4,结束流程

隐式锁

隐式锁不需要显式地编写锁定关键词,会自动加锁或解锁。
例如在未提交的事务中插入数据,就是通过隐式锁的方式避免其它事务对插入数据的读写,如果能读到了那就是脏读,如果能写那就是脏写。
隐式锁是一种延时加载的方式,在多个事务的情况下,在第二个事务访问时,才会被动的加锁,这样可以减少锁的数量。
由于不需要手动处理,了解机制就行。

步骤 会话A 会话B 会话C 备注
1 start transaction; start transaction; SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; AB开启事务,C检测锁
2 insert into cs(num1, num2, s1, s2) values(1,2,3,4); / SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; A插入数据,此时C未检测到有锁
3 / select * from cs lock in share mode; SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; insert操作会产生一个写锁,进而阻止事务B的读锁,此时事务C可以查看的到
4 / 阻塞 / 在第二个事务访问时,事务A被动的加锁,通过事务C可以查看的到
5 commit; commit; / 双方提交事务,会话A成功插入一条数据

显式锁

显式声明的锁,for update,lock in share mode这种的。

全局锁

全局锁(Global Lock)的作用是锁定整个数据库实例,让整个库只能读不能写,粒度很大。
flush tables with read lock:获取全局读锁,对所有事务只能读不能写。
unlock tables:释放全局锁。
应用场景:备份数据库或者导出数据时,为了保证数据的一致性而需要锁定整个数据库实例,确保备份过程中数据不会发生变化。全局锁会影响数据库的正常运行。
测试:

步骤 会话A 会话B 备注
1 flush tables with read lock; / 会话A开启全局锁
2 / select * from cs;desc cs; 读数据表和表数据都没问题
3 / insert into cs(num1, num2, s1, s2) values(1,2,3,4); 会话B插入数据
4 / 阻塞 全局锁禁止写操作
5 unlock tables; 插入成功 释放全局锁。数据插入成功
6 flush tables with read lock; / 会话A开启全局锁
7 / alter table cs add index(num1); 会话B为表字段加索引
8 / 阻塞; 全局锁阻塞DDL操作
9 unlock tables; 索引添加成功 释放全局锁,索引插入成功,结束会话

死锁

之前写过关于死锁的详细文章:MySQL死锁。
MySQL 中的死锁是指两个或多个事务相互等待对方释放锁资源,导致它们永远无法继续执行的情况。(不会耕田,怎能下地 不会下地,怎么耕田)
测试:先cs表中有id为1,6的两条数据

步骤 会话A 会话B 备注
1 start transaction; start transaction; 双方开启事务
2 update cs set num1 = 1 where id = 1; update cs set num1 = 60 where id = 6; 更新数据默认会产生X锁,这一步正常执行
3 update cs set num1 = 6 where id = 6; update cs set num1 = 10 where id = 1; 更新数据默认会产生X锁
4 阻塞 / 事务B对于id=6的写锁并未释放,此时又新增了update请求,需要加X锁,所以阻塞
5 / Deadlock found when trying to get lock; try restarting transaction 事务AB互相需要等待,导致死锁
6 commit; commit; 双方提交事务,结束流程,事务A正确执行,事务B被回滚

排查方法1

show engine innodb status;

执行以上命令会得到大量日志,在LATEST DETECTED DEADLOCK与TRANSACTIONS之间寻找sql 语句,以此定位死锁源头。示例如下:

......
------------------------
LATEST DETECTED DEADLOCK
------------------------
2024-01-23 23:48:30 0x1f00
*** (1) TRANSACTION:
TRANSACTION 805714, ACTIVE 12 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 3, OS thread handle 8076, query id 762 localhost ::1 root updating
update test set name = 'C++语言' where id = 2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2575 page no 3 n bits 72 index PRIMARY of table `temp`.`test` trx id 805714 lock_mode X locks rec but not gap waiting
Record lock, heap no 6 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000002; asc     ;;
 1: len 6; hex 0000000c4b53; asc     KS;;
 2: len 7; hex 34000002a80923; asc 4     #;;
 3: len 3; hex 432b2b; asc C++;;

*** (2) TRANSACTION:
TRANSACTION 805715, ACTIVE 9 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 4, OS thread handle 7936, query id 766 localhost ::1 root updating
delete from test where id = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 2575 page no 3 n bits 72 index PRIMARY of table `temp`.`test` trx id 805715 lock_mode X locks rec but not gap
Record lock, heap no 6 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000002; asc     ;;
 1: len 6; hex 0000000c4b53; asc     KS;;
 2: len 7; hex 34000002a80923; asc 4     #;;
 3: len 3; hex 432b2b; asc C++;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2575 page no 3 n bits 72 index PRIMARY of table `temp`.`test` trx id 805715 lock_mode X locks rec but not gap waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;
 1: len 6; hex 0000000c4b46; asc     KF;;
 2: len 7; hex ad000002b10110; asc        ;;
 3: len 1; hex 43; asc C;;

*** WE ROLL BACK TRANSACTION (1)
------------
TRANSACTIONS
------------
Trx id counter 805727
Purge done for trx's n:o < 805727 undo n:o < 0 state: running but idle
History list length 36
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 283132143509864, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 283132143508992, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 283132143508120, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 283132143507248, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
......

排查方法2

show status like 'innodb_row_lock%'

执行以上命令后会得到一个表格,

Innodb_row_lock_current_waits	0    如果这个数字非零,表示有事务正在等待其他事务持有的行级锁。
Innodb_row_lock_time	       18756 以毫秒为单位。它表示所有事务在竞争锁资源时,所消耗的总时间
Innodb_row_lock_time_avg	   3126  平均每个InnoDB行级锁的持有时间,毫秒为单位。用总的锁持有时间除以锁请求次数来计算。
Innodb_row_lock_time_max	   7921  单个InnoDB行级锁的最大持有时间,毫秒为单位。这个值表示所有事务竞争锁资源中的最长时间。
Innodb_row_lock_waits      	   6     这个指标表示有多少次事务在竞争锁资源时需要等待。

排查方法3
该查询是用来获取当前正在运行的事务(INNODB_TRX表)、空闲状态的线程(PROCESSLIST表,COMMAND为Sleep)、线程对应的进程信息(threads表)、线程对应的当前执行中的SQL语句(events_statements_current表)的一些相关信息。

SELECT a.trx_id, a.trx_state, a.trx_started, a.trx_query, b.ID, b.USER, b.DB, b.COMMAND, b.TIME, b.STATE, b.INFO, c.PROCESSLIST_USER, c.PROCESSLIST_HOST, c.PROCESSLIST_DB, d.SQL_TEXT 
FROM information_schema.INNODB_TRX a
LEFT JOIN information_schema.PROCESSLIST b ON a.trx_mysql_thread_id = b.id AND b.COMMAND = 'Sleep'
LEFT JOIN performance_schema.threads c ON b.id = c.PROCESSLIST_ID
LEFT JOIN performance_schema.events_statements_current d ON d.THREAD_ID = c.THREAD_ID;

解决
MySQL会自动解决死锁问题,代价就是MySQL自行找到成本最低的事务,自动回滚。所以说解决方案不是解锁,而是避免。

避免

  • 降低事务粒度:轻量级的事务,锁定更少的资源,不容易发生死锁。
  • 尽快提交事务:锁能更快的释放。
  • 合理的索引设计: 合理设计数据库表的索引可以减少锁竞争,提高查询效率。
  • 一致的访问顺序: 当应用程序涉及多个表时,保持一致的访问顺序可以避免死锁。例如,如果事务A先锁定表X,再锁定表Y,那么事务B也应该按照相同的顺序锁定表X和表Y,从而避免死锁。

什么是最小成本回滚策略?
在 MySQL 中,当发生死锁时,MySQL 使用一种叫做"最小成本回滚"(InnoDB 中称为"最小编号事务回滚")的策略来选择一个事务作为牺牲者并进行回滚,最小成本回滚策略是 MySQL 的默认行为,它会自动选择牺牲者并回滚事务。
最小成本回滚策略的原理是选择最小成本的事务作为牺牲者。评估算法如下:

  • 回滚事务所涉及的操作数量:回滚操作的数量越小,成本越低。
  • 回滚事务所占用的系统资源:回滚事务占用的系统资源越少,成本越低。
  • 回滚事务已执行的工作量:已执行的工作量越少,成本越低。

锁排查与监控

查询语句用于查看 InnoDB 存储引擎中关于行锁的相关统计信息

show status like 'innodb_row_lock%';

Innodb_row_lock_current_waits  如果这个数字非零,表示有事务正在等待其他事务持有的行级锁。
Innodb_row_lock_time	       以毫秒为单位。它表示所有事务在竞争锁资源时,所消耗的总时间
Innodb_row_lock_time_avg	   平均每个InnoDB行级锁的持有时间,毫秒为单位。用总的锁持有时间除以锁请求次数来计算。
Innodb_row_lock_time_max	   单个InnoDB行级锁的最大持有时间,毫秒为单位。这个值表示所有事务竞争锁资源中的最长时间。
Innodb_row_lock_waits      	   这个指标表示有多少次事务在竞争锁资源时需要等待。

查看 InnoDB 存储引擎中当前活动的事务信息

SELECT * FROM information_schema.innodb_trx;

trx_id                         事务的唯一标识符。
trx_state                      事务的状态,如 RUNNING、LOCK WAIT、ROLLING BACK 等。
trx_started                    事务启动的时间。
trx_requested_lock_id          请求的锁的标识符。
trx_wait_started               等待锁的开始时间。
trx_weight                     事务的权重,用于死锁检测。
trx_mysql_thread_id            MySQL 线程 ID。
trx_query                      与事务相关的 SQL 查询语句。
trx_operation_state            事务内部操作的状态。
trx_tables_in_use              事务使用的表的数量。
trx_tables_locked              事务锁定的表的数量。
trx_lock_structs               事务内部使用的锁结构数量。
trx_lock_memory_bytes          用于事务锁定的内存字节数。
trx_rows_locked                事务锁定的行数。
trx_rows_modified              事务修改的行数。
trx_concurrency_tickets        用于事务并发控制的票数。
trx_isolation_level            事务的隔离级别。
trx_unique_checks              是否启用了唯一性检查。
trx_foreign_key_checks         是否启用了外键约束检查。
trx_last_foreign_key_error     最后一个外键错误信息。
trx_adaptive_hash_latched      是否适应性哈希被锁定。
trx_adaptive_hash_timeout      适应性哈希锁定超时次数。
trx_is_foreign_key_with_check  是否用于外键约束检查。
trx_is_foreign_key             是否用于外键约束。

查看 InnoDB 存储引擎中当前正在被锁定的对象(如表、行)的信息

SELECT * FROM information_schema.innodb_locks;

lock_id      锁的唯一标识符。
lock_trx_id  持有该锁的事务的唯一标识符。
lock_mode    锁的模式,如 S(共享锁)或 X(独占锁)等。
lock_type    锁的类型,如 RECORD(行级锁)或 TABLE(表级锁)等。
lock_table   被锁定的表名。
lock_index   被锁定的索引名。
lock_space   被锁定的表空间的标识符。
lock_page    被锁定的页码。
lock_rec     锁定的记录。
lock_data    与锁相关的其他数据。

查看 InnoDB 存储引擎中当前存在的锁等待情况

SELECT * FROM information_schema.innodb_lock_waits;

requesting_trx_id   请求锁的事务的唯一标识符。
requested_lock_id   请求的锁的唯一标识符。
blocking_trx_id     导致锁等待的阻塞事务的唯一标识符。
blocking_lock_id    导致锁等待的锁的唯一标识符。

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论