百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

MySQL InnoDB的加锁机制

csdh11 2025-02-06 14:30 9 浏览

日常工作中,经常会遇到各种死锁的场景,有的死锁分析起来是比较容易的,比如同类型的事务并发引起的死锁;而不同类型事务并发引起的死锁,分析起来就不是那么容易了。系统化的了解数据库的加锁机制,不仅有助于对现有问题的分析,在设计阶段也能更好的把握系统的性能与复杂业务场景的解决方案。

要掌握数据库的加锁机制,我们需要弄明白三件事:

  • 有哪些类型的锁
  • 对什么东西加锁
  • 以什么样的方式加锁

有哪些类型的锁

MySQL InnoDB一共有四种锁:共享锁(读锁,S锁)、排他锁(写锁,X锁)、意向共享锁(IS锁)和意向排他锁(IX锁)。其中共享锁与排他锁属于行级锁,另外两个意向锁属于表级锁。

  • 共享锁(读锁,S锁)
  • 若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放S锁。
  • 排他锁(写锁,X锁)
  • 若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他事务不能再对A加任何类型的锁,直到T释放A上的X锁。
  • 意向共享锁(IS锁)
  • 事务T在对表中数据对象加S锁前,首先需要对该表加IS(或更强的IX)锁。
  • 意向排他锁(IX锁)
  • 事务T在对表中的数据对象加X锁前,首先需要对该表加IX锁。

比如SELECT ... FROM T1 LOCK IN SHARE MODE语句,首先会对表T1加IS锁,成功加上IS锁后才会对数据加S锁。

同样,SELECT ... FROM T1 FOR UPDATE语句,首先会对表T1加IX锁,成功加上IX锁后才会对数据加X锁。

MySQL官网上有个死锁的例子,但分析得过于概括,这里我们详细分析一下。

首先,会话S1以SELECT * FROM t WHERE i = 1 LOCK IN SHARE MODE查询,该语句首先会对t表加IS锁,接着会对数据(i = 1)加S锁。

mysql> CREATE TABLE t (i INT) ENGINE = InnoDB;  
Query OK, 0 rows affected (1.07 sec)  


mysql> INSERT INTO t (i) VALUES(1);  
Query OK, 1 row affected (0.09 sec)  


mysql> START TRANSACTION;  
Query OK, 0 rows affected (0.00 sec)  


mysql> SELECT * FROM t WHERE i = 1 LOCK IN SHARE MODE;  
+------+  
| i    |  
+------+  
|    1 |  
+------+  
1 row in set (0.10 sec)

接着,会话S2执行DELETE FROM t WHERE i = 1,该语句尝试对t表加IX锁,由于IX锁与IS锁是兼容的,所以成功对t表加IX锁。接着继续对数据(i = 1)加X锁,但数据已经被会话S1事务加了S锁了,所以会话S2等待。

mysql> START TRANSACTION;  
Query OK, 0 rows affected (0.00 sec)  


mysql> DELETE FROM t WHERE i = 1;

接着,会话S1也执行DELETE FROM t WHERE i = 1,该语句首先对t表加IX锁,虽然会话S2已经对t表加了IX锁,但IX锁与IX锁之间是兼容的,所以成功对t表加上IX锁。接着会话S1会对数据(i = 1)加X锁,此时发现会话S2占有的IX锁与X锁不兼容,所以会话S1也等待。

就这样,会话S1等S2释放IX锁,而会话S2等S1释放S锁,从而死锁发生。

mysql> DELETE FROM t WHERE i = 1;  
Query OK, 1 row affected (0.00 sec)  


mysql>

上例中会话S1虽然执行成功了,但是下面会话S2发生了死锁。这是因为Mysql检测到死锁后,会自动强制其中一个事务退出。

mysql> DELETE FROM t WHERE i = 1;  
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction  
mysql>

对什么加锁

每一张InnoDB表都有且仅有一个特殊的索引,聚族索引(Clustered Index),表中的数据是直接存放在聚族索引的叶子节点中,这样,根据聚族索引查询就会比普通索引更快,因为少了一次IO操作。

通常,聚族索引就是表的主键;如果表没有主键,那InnoDB会把第一个非空的唯一索引当作聚族索引;如果表既无主键,又无非空的唯一索引,那么InnoDB会创建一个隐藏的索引作为聚族索引。表中的其它索引,都叫做第二索引(Secondary Index),第二索引中只包含自身索引列和聚族索引列的内容,所以当一个表的主键很长时,其它的索引都会受到影响。

为什么要先讲聚族索引呢?因为这对理解InnoDB加锁机制很重要,InnoDB加锁的对象不是返回的数据记录,而是查询这些数据时所扫描过的索引。当我们执行一个锁读(select … lock in share mode或者select … for update)时,InnoDB不是对最终的返回结果加锁,而是对查询这些结果时所扫描的索引加锁,如果被扫描的索引不是聚族索引,那被扫描的索引以及它所指向的聚族索引也会被加锁

由此可知,当一个锁读无法使用索引的话,InnoDB就是遍历整个表(遍历整个聚族索引),从而把整张表都锁住。

以什么样的方式加锁

MySQL InnoDB支持三种锁定方式:

  • 行锁(Record Lock)
  • 锁直接加在索引上。
  • 间隙锁(Gap Lock)
  • 锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间。间隙锁的作用是防止其它事务的插入操作,以此来防止幻读的发生,所以间隙锁不区分共享锁与排它锁。
  • Next-Key Lock:行锁与间隙锁组合起来用就叫做Next-Key Lock。

默认情况下,InnoDB工作在可重复读隔离级别下,并且以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是行锁与间隙锁的组合,这样,当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上行锁,再对索引记录两边的间隙加上间隙锁。如果一个间隙被事务T1加了锁,其它事务是不能在这个间隙插入记录的。

还是用之前的员工表举例:

id

num

depart

name

10

1010

5100

张三

20

1020

5200

李四

30

1030

5300

王五

40

1040

5100

刘大

其中,id表示记录主键;num表示员工工号,唯一索引;depart表示员工所在的部门编号,普通索引;name表示员工姓名。测试前插入一些测试数据。

depart索引可以表示为下面一张二维表:

depart

5100

5100

5200

5300

ID

10

40

20

30

由上面的表可知,depart索引将整个间隙拆分为五个区间:

  • (-∞, [5100|10])
  • ([5100|10], [5100|40])
  • ([5100|40], [5200|20])
  • ([5200|20], [5300|30])
  • ([5300|30], +∞)


现在我们在可重复读级别下,根据索引字段depart = 5100加锁查询的话,应该会锁定前三个间隙:

mysql> set autocommit=0;
Query OK, 0 rows affected (0.01 sec)


mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)


mysql> select * from employee where depart = 5100 lock in share mode;
+----+------+--------+--------+
| id | num  | depart | name   |
+----+------+--------+--------+
| 10 | 1010 |   5100 | 张三   |
| 40 | 1040 |   5100 | 刘大   |
+----+------+--------+--------+
2 rows in set (0.00 sec)

另起一个会话测试一下:

mysql> insert into employee values (1, 9999, 5000, 'xx');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into employee values (15, 9999, 5100, 'xx');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into employee values (55, 9999, 5100, 'xx');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into employee values (55, 9999, 5150, 'xx');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into employee values (15, 9999, 5200, 'xx');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into employee values (25, 9999, 5200, 'xx');
Query OK, 1 row affected (0.00 sec)


mysql> select * from employee where id = 10 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from employee where id = 40 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from employee where depart = 5100 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql>

其中(5000, 1)命中第一个间隙(-∞, [5100|10])

(5100, 15)命中第二个间隙([5100|10], [5100|40])

(5100, 55)、(5150, 55)与(5200, 15)都是命中第三间隙([5100|40], [5200|20])

所以前五个插入都由于锁等待超时而失败,直到(5200, 25)才成功。

间隙锁在InnoDB的唯一作用就是防止其它事务的插入操作,以此来达到防止幻读的发生,所以间隙锁不分什么共享锁与排它锁。

在上面的例子中,我们选择的是一个普通(非唯一)索引字段来测试的。这不是随便选的,如果InnoDB扫描的是一个主键、或是一个唯一索引的话,那InnoDB只会采用行锁方式来加锁,而不会使用Next-Key Lock的方式,也就是说不会对索引之间的间隙加锁,大家可以想想为什么?

最后注意看上例中最后的三个查询也超时了,表示索引depart = 5100,及它所指向的聚族索引id = 10 & id = 40都被加了读锁(行锁)。

间隙锁存在的意义是为了解决幻读的问题,在读已提交级别下,InnoDB是不会使用间隙锁的,因为这个级别本身不要求避免幻读的发生,所以在读已提交级别下,InnoDB只会以行锁的方式对索引加锁,不会使用间隙锁。

总结

  • select * from … lock in share mode对索引加共享锁;select * from ... for update对索引加排他锁。
  • update与delete对索引加排他锁。
  • insert into … 对间隙加意向插入锁(相当于范围为1的间隙锁)。
  • 默认情况下select * from ... 是非阻塞式读(Serializable除外),不会对索引加锁。在读已提交级别下,总是查询记录的最新、有效的版本;在可重复读级别下,会记住第一次查询时的版本,之后的查询会基于该版本。例外的情况是在Serializable级别,这时会以Next-Key Lock的方式对索引加锁。
  • 在可重复读级别下,InnoDB以Next-Key Lock的方式对索引加锁;在读已提交级别下,InnoDB以Index-Record Lock的方式对索引加锁。
  • 被加锁的索引如果不是聚族索引,那索引所指向的聚族索引也会被加锁(如果是索引覆盖查询,则不会对聚族索引加锁)。

相关推荐

探索Java项目中日志系统最佳实践:从入门到精通

探索Java项目中日志系统最佳实践:从入门到精通在现代软件开发中,日志系统如同一位默默无闻却至关重要的管家,它记录了程序运行中的各种事件,为我们排查问题、监控性能和优化系统提供了宝贵的依据。在Java...

用了这么多年的java日志框架,你真的弄懂了吗?

在项目开发过程中,有一个必不可少的环节就是记录日志,相信只要是个程序员都用过,可是咱们自问下,用了这么多年的日志框架,你确定自己真弄懂了日志框架的来龙去脉嘛?下面笔者就详细聊聊java中常用日志框架的...

物理老师教你学Java语言(中篇)(物理专业学编程)

第四章物质的基本结构——类与对象...

一文搞定!Spring Boot3 定时任务操作全攻略

各位互联网大厂的后端开发小伙伴们,在使用SpringBoot3开发项目时,你是否遇到过定时任务实现的难题呢?比如任务调度时间不准确,代码报错却找不到方向,是不是特别头疼?如今,随着互联网业务规模...

你还不懂java的日志系统吗 ?(java的日志类)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

谈谈枚举的新用法--java(java枚举的作用与好处)

问题的由来前段时间改游戏buff功能,干了一件愚蠢的事情,那就是把枚举和运算集合在一起,然后运行一段时间后buff就出现各种问题,我当时懵逼了!事情是这样的,做过游戏的都知道,buff,需要分类型,且...

你还不懂java的日志系统吗(javaw 日志)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

Java 8之后的那些新特性(三):Java System Logger

去年12月份log4j日志框架的一个漏洞,给Java整个行业造成了非常大的影响。这个事情也顺带把log4j这个日志框架推到了争议的最前线。在Java领域,log4j可能相对比较流行。而在log4j之外...

Java开发中的日志管理:让程序“开口说话”

Java开发中的日志管理:让程序“开口说话”日志是程序员的朋友,也是程序的“嘴巴”。它能让程序在运行过程中“开口说话”,告诉我们它的状态、行为以及遇到的问题。在Java开发中,良好的日志管理不仅能帮助...

吊打面试官(十二)--Java语言中ArrayList类一文全掌握

导读...

OS X 效率启动器 Alfred 详解与使用技巧

问:为什么要在Mac上使用效率启动器类应用?答:在非特殊专业用户的环境下,(每天)用户一般可以在系统中进行上百次操作,可以是点击,也可以是拖拽,但这些只是过程,而我们的真正目的是想获得结果,也就是...

Java中 高级的异常处理(java中异常处理的两种方式)

介绍异常处理是软件开发的一个关键方面,尤其是在Java中,这种语言以其稳健性和平台独立性而闻名。正确的异常处理不仅可以防止应用程序崩溃,还有助于调试并向用户提供有意义的反馈。...

【性能调优】全方位教你定位慢SQL,方法介绍下!

1.使用数据库自带工具...

全面了解mysql锁机制(InnoDB)与问题排查

MySQL/InnoDB的加锁,一直是一个常见的话题。例如,数据库如果有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?下面是不同锁等级的区别表级锁:开销小,加锁快;不会出现死锁;锁定粒度...

看懂这篇文章,你就懂了数据库死锁产生的场景和解决方法

一、什么是死锁加锁(Locking)是数据库在并发访问时保证数据一致性和完整性的主要机制。任何事务都需要获得相应对象上的锁才能访问数据,读取数据的事务通常只需要获得读锁(共享锁),修改数据的事务需要获...