MySQL InnoDB锁实现及原理深度剖析
MySQL InnoDB锁实现及原理深度剖析
一、引言
在数据库系统中,锁是保证数据一致性和隔离性的核心机制。MySQL的InnoDB存储引擎以其卓越的行级锁和高并发处理能力著称。然而,很多数据库开发者对锁的理解往往停留在“加锁会阻塞”的层面,对于锁在内存中长什么样、如何组织、如何等待、如何检测死锁等底层原理知之甚少。
本文将深入剖析InnoDB锁的实现原理。建立起完整的锁知识体系,从而在性能优化、死锁排查时能够游刃有余。
二、锁的硬件基石:从MESI到内存屏障
在讨论InnoDB的锁之前,我们必须先理解锁的底层实现依赖于硬件和操作系统的支持。锁的本质是解决并发编程中对共享资源的互斥访问。
2.1 缓存一致性协议(MESI)
在多核处理器系统中,每个CPU核心都有自己的L1/L2缓存。如果两个核心同时读取内存中的同一个变量,它们都会缓存这个变量的副本。当其中一个核心修改了这个变量,如何保证另一个核心知道这个变化?这就是缓存一致性协议要解决的问题。

缓存行(Cache line)
- 缓存行是指在缓存中的最小数据单元。
最常见的MESI协议定义了四种缓存行状态:
| 四种缓存状态 | 描述 | 监听任务 |
|---|---|---|
| E 独享 | 该Cache line有效,数据被修改,和内存数据一致,数据只存在本Cahe中 | 必须监听所有试图读该缓存行的操作,操作必须在该缓存行写回主存并将状态变为S后执行 |
| M 修改 | 该Cache line有效,数据被修改,和内存数据不一致,数据只存在本Cahe中 | 必须监听所有试图读该缓存行的操作,操作必须在该缓存行写回主存并将状态变为S后执行 |
| S 共享 | 该Cache line有效,数据和内存数据一致,数据存在多个Cache中 | 必须监听其它缓存使该缓存无效或独享该缓存的请求,并将该缓存行变为无效 |
| I 失效 | 该Cache line无效 | 无 |
注意:
对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。如果一个处于S状态的缓存失效,另外一个缓存行可能已经独享了该缓存行,但是不会升迁为独享状态,因为失效并不会立即广播给其它缓存行。

当一个核心修改了处于Shared状态的变量时,它会发送消息通知其他核心将该变量的缓存行置为Invalid状态。这就是硬件级别的锁基础。
2.2 原子操作与CAS
InnoDB的锁实现离不开原子操作。在x86架构下,通过LOCK前缀可以实现原子指令。例如LOCK CMPXCHG(Compare and Exchange)是CAS(Compare-And-Swap)操作的硬件实现。
CAS操作的原型:CAS(address, expected, new)。其语义是:如果地址address处的值等于expected,则将其更新为new,并返回true;否则返回false。整个操作是原子的。
InnoDB在实现信号量(即线程间同步机制)、spin lock(自旋锁)时,会大量使用CAS操作。例如,InnoDB自定义的os_atomic_test_and_set函数底层就是封装了CAS指令。
2.3 内存屏障
由于编译器和CPU都可能对指令进行重排序,为了保证锁的正确性,需要内存屏障指令。x86架构中,mfence指令保证所有之前的内存操作完成后才执行之后的内存操作。InnoDB在实现自己的读写锁(rw_lock)时,会在关键位置插入内存屏障,防止因重排序导致的锁状态不一致。
三、InnoDB锁类型全解
InnoDB的锁类型丰富多样,从不同维度可以分为:表锁与行锁、共享锁与排他锁、意向锁、间隙锁、Next-Key锁、插入意向锁、自增锁等。
3.1 全局锁(Global Lock)
全局锁是对整个MySQL实例加锁,最典型的是通过 FLUSH TABLES WITH READ LOCK(简称FTWRL)命令实现的全局读锁。执行该命令后,整个数据库处于只读状态,所有对数据的写操作(包括DML和DDL)都会被阻塞。全局锁通常用于全库备份,以保证备份数据的一致性。
3.1.1 实现原理
FTWRL的执行过程大致如下:
- 加全局读锁:MySQL Server层会获取一个全局的读锁,本质上是向所有表加锁,并阻塞所有写操作。
- 刷新表缓存:关闭所有打开的表,并刷新表缓存。
- 返回成功:之后所有对表的写请求都会进入等待状态,直到释放全局读锁(通过
UNLOCK TABLES或会话结束)。
在InnoDB内部,全局锁的实现并不依赖于复杂的行锁结构,而是通过一个简单的计数器或标志位来控制。当持有全局读锁时,所有需要获取写锁(包括表锁的IX锁、X锁)的请求都会在Server层被阻塞,不会进入存储引擎层。
3.1.2 与其它锁的关系
全局锁是最高优先级的锁,它会影响所有表,包括MyISAM、InnoDB等。在InnoDB中,即使某张表上没有活动事务,只要全局锁存在,任何修改操作都无法进行。
3.2 表锁
3.2.1 表锁 - 表共享读锁/表共享写锁
用户可以通过 LOCK TABLES tbl_name READ | WRITE 显式地为表加锁。这类锁由Server层管理,但会下推到InnoDB层。
- READ锁:相当于表级的共享锁(S锁)。持有该锁的会话可以读表,但不能写;其他会话可以读,但写会被阻塞。
- WRITE锁:相当于表级的排他锁(X锁)。持有该锁的会话可以读写表,其他会话的所有操作都被阻塞。
当执行 LOCK TABLES ... WRITE 时,InnoDB会先获取表的IX锁(因为意图修改行),然后再获取表级的X锁。但需要注意,LOCK TABLES 会隐式提交当前事务,并且解锁只能通过 UNLOCK TABLES 或会话结束。
READ锁:该锁的会话可以读表,但不能写;其他会话可以读,但写会被阻塞

WRITE锁:持有该锁的会话可以读写表,其他会话的所有操作都被阻塞。

3.2.2 表锁 - 元数据锁
MDL是Server层的锁,它的加锁过程是系统自动控制的,用于保护数据库对象的元数据(如表结构),为了避免DML与DDL冲突,保证读写的正确性。
当执行DDL语句(如ALTER TABLE)或DML语句(如SELECT、UPDATE)时,会自动获取MDL。
MDL分为:
- MDL读锁(共享 SHAREN_READ,SHAREN_WRITE):DML语句需要MDL读锁,允许并发执行。
- MDL写锁(排他 EXCLUSIVE):DDL语句需要MDL写锁,会阻塞其他所有MDL读锁和写锁。
下表是各个类型的SQL时,加不同类型的元数据锁:
| 对应SQL类型 | 锁类型 | 说明 |
|---|---|---|
| lock tables xxx read/write | SHAREB_READ_ONLY/SHARED_NO_READ_WRITE | |
| select 、 select … lock in share mode | SHARED_READ | 与SHARED_READ、SHARED_WRITE兼容,与EXCLUSIVE互斥 |
| insert、update、delete、select … for update | SHARED_WRITE | 与SHARED_READ、SHARED_WRITE兼容,与EXCLUSIVE互斥 |
| alter table | EXCLUSIVE | 与其他的MDL互斥 |
MDL与InnoDB的表锁是两个独立但协作的锁系统。MDL保证了表结构在语句执行期间不被修改,而InnoDB的表锁(意向锁)保证了行锁与表锁的兼容性。两者的关系可概括为:
- MDL锁在Server层,InnoDB表锁在存储引擎层。
- 执行DML时,先加MDL读锁,再进入InnoDB加意向锁。
- 执行DDL时,先加MDL写锁(等待所有MDL读锁释放),然后可能还需要等待InnoDB的表锁释放(如果DDL需要排他访问表数据)。
3.2.3 表锁 - 意向锁
意向锁是表级别的锁,它解决的是多粒度锁共存的问题。
InnoDB支持行锁和表锁共存,如果没有意向锁,当需要给整个表加X锁时,就必须遍历所有行锁来判断是否有行被锁定,这显然效率极低。
比如下面这张图,线程A给id为3的数据加上了行数,此时线程B想给这张表加一个表锁,那么他就需要遍历整个表的数据,判断是否有行数。

在加入意向锁之后,线程A加上行锁时,会同时加上意向锁,而线程B检测到这张表加了意向锁,会阻塞,直到线程A释放意向锁。

意向锁的引入,使得表锁和行锁的协作变得高效。它分为两种:
- 意向共享锁(Intention Shared Lock,IS锁):表示事务意图在表的某些行上设置共享锁。例如
SELECT ... LOCK IN SHARE MODE会先获取表的IS锁。 - 意向排他锁(Intention Exclusive Lock,IX锁):表示事务意图在表的某些行上设置排他锁。例如
SELECT ... FOR UPDATE、INSERT、UPDATE、DELETE都会先获取表的IX锁。
意向锁的协议非常简单且重要:事务在获取行上的S锁之前,必须先获取表上的IS锁或更强的锁;在获取行上的X锁之前,必须先获取表上的IX锁。
表级锁类型的兼容性矩阵如下:
| X | IX | S | IS | |
|---|---|---|---|---|
| X | 冲突 | 冲突 | 冲突 | 冲突 |
| IX | 冲突 | 兼容 | 冲突 | 兼容 |
| S | 冲突 | 冲突 | 兼容 | 兼容 |
| IS | 冲突 | 兼容 | 兼容 | 兼容 |
关键发现:意向锁之间是互相兼容的!因为IX和IX都是表示“意图”做某事,而非真正的锁定。两个事务都可以有“意图”在同一个表上加排他锁,这并不妨碍它们,它们只是在不同的行上操作罢了
3.3 行锁
行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高,应用在InnoDB存储引擎中。
InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对应行级锁,主要分成三类:行锁、间隙锁、
3.3.1 行锁(Record Lock)
行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RR和RC隔离级别下都支持
比如下面这条数据,会将34这条数据锁住,而其他事务无法对这条数据进行update和delete。

InnoDB实现了两种类型的行锁:
共享锁(Shared Lock,S锁):允许持有锁的事务读取行。
排他锁(Exclusive Lock,X锁):允许持有锁的事务更新或删除行。
它们的兼容性矩阵如下:
| S锁(共享锁) | X锁(排他锁) | |
|---|---|---|
| S锁(共享锁) | 兼容 | 冲突 |
| X锁(排他锁) | 冲突 | 冲突 |
不同SQL类型加的行锁类型:
| SQL | 行锁类型 | 说明 |
|---|---|---|
| insert … | 排他锁 | 自动加锁 |
| update … | 排他锁 | 自动加锁 |
| delete … | 排他锁 | 自动加锁 |
| select … | 不加任何锁 | |
| select … lock in share mode | 共享锁 | 需要手动在select之后加lock in share mode |
| select … for update | 排他锁 | 需要手动在select之后加for update |
3.3.2 间隙锁(Gap Lock)
间隙锁:锁住两条记录之间的间隙,但不包括记录本身。它是“纯抑制性”的,唯一目的是防止其他事务向间隙中插入数据,产生幻读,在在RR隔离级别下支持。
比如下图中,对29,34两条数据之间的添加锁,保证了两条记录之间间隙不变

3.3.3 临键锁(Next-Key锁)
Next-Key锁:记录锁和间隙锁的组合,锁住一个范围,并且包含记录本身。
比如下图中,对29,34两条数据之间的添加锁,同时对34这条数据添加了锁

默认情况下,InnoDB在RR事务的隔离级别下运行,InnoDB使用 next key 锁进行搜索和索引扫描,以防止幻读。
索引上的等值查询(唯一索引),给不存在的记录加锁时,优化为间隙锁。
比如如下图:我们对一个不存在的id为5 的数据进行更新操作。
然后我们查询,这张表的锁,可以看到已经加上了排他锁和间隙锁,并且锁住的是3-8之间的数据。
然后,我们执行insert id为7的语句时,发现SQL执行阻塞住了。

索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock退化为间隙锁。
对age字段创建一个普通索引,然后查询。

然后我们查询这张表的锁,发现有3个锁
第一个锁:对3之间的数据及这条记录给锁住
第二个锁:对当前的这条3加上锁
第三个锁:间隙锁,将3和7之间的数据给数据
为什么要加这些锁呢?主要是因为age这个字段是非唯一索引,可以其他的事务 会在3-7只间的间隙中插入数据

- 索引上的范围查询(唯一索引) – 会访问到不满足条件的第一个值为止。
我们基于逐渐id查询,查询范围大于等于19的数据。

我们看下会加上那些锁:
- 首先会对19这条数据加行锁
- 然后第二个锁,supermum pseudo-record, 对25之后的数据加上临键锁
- 最后会对25加临键锁

四、锁的请求与授予
下面我们来看看一个事务是如何请求并获取锁的。
4.1 加锁流程概览
当一个事务执行一条SQL(例如 UPDATE t1 SET name='a' WHERE id=10),InnoDB决定需要对记录加X锁时,会经历以下步骤:
- 加表级意向锁:首先,事务需要获取表
t1的IX锁。它会在表的locks链表中检查是否存在冲突的锁(如S锁或X锁)。如果没有,则创建或复用IX锁结构,加入链表,状态为GRANTED。 - 查找行锁哈希表:接下来,根据索引
PRIMARY和记录id=10所在的页(space_id,page_no),计算出哈希值,找到lock_sys->rec_hash中对应的槽位。 - 遍历行锁链表:遍历该槽位下的行锁链表,寻找针对同一个页的锁结构。
- 判断锁冲突:
- 如果在链表中找到了一个锁结构,其
type_mode与当前请求不兼容,并且该锁结构的状态为GRANTED,则当前请求必须等待。 - 如果没有找到任何冲突的锁结构,则需要创建或复用锁结构。
- 如果在链表中找到了一个锁结构,其
- 创建/复用锁结构:
- 检查是否有针对这个页的、
type_mode与当前请求完全相同的锁结构。如果有,则尝试通过位图“合并”。(例如,如果另一个X锁结构已经锁住了这个页的id=5,现在想锁id=10,如果模式相同,就在那个锁结构的位图中将id=10对应的位设置为1,实现复用)。 - 如果没有可复用的,则创建新的
lock_t对象,初始化位图,并将其插入到rec_hash对应槽位的链表头部,同时也插入到事务的trx_locks链表头部。
- 检查是否有针对这个页的、
4.2 锁结构复用条件
多个锁结构能合并共用的条件非常严格,必须同时满足:
- 属于同一个事务。
- 作用于同一个索引的同一个数据页。
- 锁的基本模式相同(同为S锁或同为X锁)。
- 锁的精确模式相同(同为记录锁、或同为间隙锁、或同为Next-Key锁)。
- 锁的等待状态相同(同为已授予或同为等待)。
这种精细的复用机制,使得InnoDB在加大量行锁时,内存开销远小于行数。
4.3 锁的等待与排队
当请求的锁因冲突无法立即授予时,事务必须进入等待状态。这个过程就像去银行办业务,需要先取号排队,然后等着叫号。
4.3.1 排队:加入等待队列
- 申请锁结构:事务会申请一个新的锁结构,这个锁结构的状态是
LOCK_WAIT(type_mode的第9位置为1)。 - 插入链表:
- 对于表锁:将这个等待的锁结构插入到表对象的
locks链表末尾。 - 对于行锁:将这个等待的锁结构插入到
rec_hash槽位对应的行锁链表末尾。
- 对于表锁:将这个等待的锁结构插入到表对象的
4.3.2 登记:利用Slot机制
仅仅加入队列还不够,InnoDB还需要一种机制来管理所有等待的线程,以便超时检测和死锁检测。这就是waiting_threads数组的作用。
lock_sys->waiting_threads指向一片连续的内存区域,里面有max_threads个slot。每个slot(srv_slot_t)可以记录一个等待事务的信息。
当发生锁等待时,InnoDB会:
- 找空闲slot:遍历
waiting_threads数组,找到一个未使用的slot(in_use == false)。 - 登记信息:将当前事务的ID、等待的锁结构、等待超时时间等信息填入slot,并将
in_use设为true。 - 等待:事务线程随后会进入条件等待(wait on condition)。它会被挂起,不再占用CPU。
这个slot机制就像一个“候诊区”的挂号单,后台线程(如锁超时检测线程)通过检查这些slot,就知道有哪些事务在等待,已经等了多久。
五、不同SQL语句的加锁规则
理解了锁的结构和请求机制,现在我们来分析实际SQL语句的加锁行为。这是最容易产生误解的地方,也是死锁和性能问题的高发区。加锁规则主要与事务隔离级别、索引类型、查询条件有关。
5.1 SELECT 查询(快照读)
在READ COMMITTED和REPEATABLE READ隔离级别下,普通的SELECT语句是快照读,通过MVCC(多版本并发控制,即通过版本链和Read View实现无锁读)读取数据,不加任何锁(除非在SERIALIZABLE级别下,普通SELECT会退化为SELECT ... LOCK IN SHARE MODE)。
5.2 锁定读:FOR SHARE 和 FOR UPDATE
锁定读取语句会显式地加锁。
SELECT ... FOR SHARE:在扫描到的索引记录上加S锁(Next-Key锁)。SELECT ... FOR UPDATE:在扫描到的索引记录上加X锁(Next-Key锁)。
5.3 加锁规则详解(以REPEATABLE READ为例)
5.3.1 主键等值查询
情况A:记录存在
sql
1
2-- id 是主键
SELECT * FROM t WHERE id = 10 FOR UPDATE;加锁结果:只在主键索引
id = 10的记录上加一个记录锁。这是一个精准锁,不需要锁间隙,因为唯一索引能唯一确定一行,幻读无从发生。情况B:记录不存在
sql
1
SELECT * FROM t WHERE id = 12 FOR UPDATE;假设表中有id=10和id=15的记录。
加锁结果:在(10,15)这个间隙上,加一个间隙锁。它防止其他事务插入id=12的记录,从而避免了幻读。
5.3.2 唯一索引等值查询
情况A:记录存在
sql
1
2-- id 是唯一索引
SELECT * FROM t WHERE id = 10 FOR UPDATE;加锁结果:
- 在唯一索引
id = 10的记录上加一个记录锁(锁定这个索引项)。 - 在主键索引对应的行记录上加一个记录锁(防止其他事务通过主键修改这条记录)。
- 在唯一索引
情况B:记录不存在 与主键不存在类似,锁住唯一索引上该值所在的间隙。
5.3.3 非唯一索引等值查询
这是最复杂的加锁场景,也是产生死锁的温床。
sql
1 | |
假设主键id是1到10,’apple’对应的id是3和5。
加锁结果:
- 遍历扫描:从非唯一索引的非叶子节点开始,定位到第一个name=’apple’的索引项。由于是非唯一索引,InnoDB必须向右扫描,直到遇到第一个不等于’apple’的值(即’banana’)才停止。这个扫描过程中访问到的所有索引项都会加锁。
- 锁的类型:
- 对于等于’apple’的索引项(id=3,5),加的是 Next-Key锁。它不仅锁住这个索引项,还锁住了它前面的间隙。例如,锁住第一个’apple’(id=3)前面的间隙(即从’ap’之前的某个值到id=3之间的间隙),以及两个’apple’之间的间隙(id=3到id=5的间隙)。
- 对于停止点,即第一个不等于’apple’的索引项(’banana’),加的是间隙锁。它锁住了最后一个’apple’(id=5)和’banana’之间的间隙。
- 同时,所有被锁的非唯一索引项对应的主键记录,也会被加上X锁。
为什么要锁间隙和下一个值? 这是为了防止幻读。如果在事务执行过程中,另一个事务插入了一条新的name=’apple’的记录(例如id=4),如果不锁住(3,5)这个间隙,这个新记录就会被插入,导致当前事务再次查询时,结果集多了id=4这条记录,也就是幻读。
5.3.4 范围查询
sql
1 | |
加锁结果:
- 扫描到的所有索引记录,加上 Next-Key锁。
- 特别地,对于第一个不满足条件的记录(例如id=20),也会被加锁(通常是间隙锁,如果是扫描过程中访问到了,就是Next-Key锁,但最终会因为不满足条件而释放?这里存在优化,但规则是:扫描到的就要加锁)。
- 最终效果是锁住了整个范围[10, 20)以及20之前的间隙。这实际上是一个范围锁。
5.4 UPDATE 和 DELETE 的加锁
UPDATE和DELETE的加锁规则和SELECT ... FOR UPDATE完全一致。它们也是锁定读取,需要先找到要修改的记录(找到的过程加锁),然后才执行修改。这就是为什么一个UPDATE语句即使在WHERE条件过滤后只有一行要更新,也可能锁住一大片范围的原因。
5.5 INSERT 的加锁
INSERT操作相对特殊,它主要涉及两个阶段的加锁:
- 插入意向锁:在插入前,会在插入的间隙上设置插入意向锁。这是一个等待机制,表示它想在这个间隙里插入数据。
- 行锁:成功插入后,会在新插入的记录上加上X锁(记录锁)。这个X锁是为了防止其他事务修改或删除这条刚刚插入的记录。
唯一键冲突:如果INSERT因为唯一键冲突而失败,InnoDB会在冲突的索引记录上加一个S锁(共享锁)。这是很多人忽略的一个点,也是导致死锁的经典场景之一。
5.6 LOCK TABLES 语句的加锁
考虑以下显式加表锁的场景:
sql
1 | |
执行流程:
- 提交当前事务:
LOCK TABLES会隐式提交当前事务。 - 加MDL写锁:Server层先获取表的MDL写锁,防止DDL并发。
- 加InnoDB表锁:InnoDB层会先获取表的IX锁(因为可能做写操作),然后获取表级的X锁(
LOCK_X)。这个X锁会记录在表对象的locks链表中。 - 执行DML:后续的DML语句不再需要单独获取行锁?实际上,在持有表级X锁的情况下,DML操作可以跳过行锁的竞争检查,直接修改数据(因为已经独占全表)。但InnoDB为了保持一致性,仍然会加行锁,只不过这些行锁不会被阻塞(因为表锁已经保证了排他性)。
- 释放锁:
UNLOCK TABLES释放所有表锁,同时也会释放MDL锁。
六、锁等待与死锁
当多个事务互相等待对方释放锁时,就会形成锁等待。如果这种等待形成了一个循环,就变成了死锁。
6.1 锁等待超时
如果一个事务等待时间超过了阈值(由参数innodb_lock_wait_timeout设置,默认50秒),InnoDB会放弃这个锁请求,并回滚该事务。InnoDB依靠后台线程定期检查waiting_threads数组中的slot,计算等待时间是否超时。
6.2 死锁检测与处理
死锁的代价比锁等待超时更大。InnoDB采用了等待图(Wait-for Graph)算法来主动检测死锁。
6.2.1 等待图的构建
等待图是一个有向图。节点是事务。有向边 T1 -> T2 表示 T1 在等待 T2 释放锁。
InnoDB在每次加锁请求不能立即满足时,就会在内存中更新这个等待图。图的信息可以从锁的等待队列中推导出来。
6.2.2 死锁检测时机
当发生锁等待(即加锁请求需要排队时),InnoDB会将新加入的等待事务作为起点,尝试进行深度优先搜索(DFS),检查图中是否存在环。如果存在环,则说明发生了死锁。
6.2.3 选择牺牲者
一旦检测到死锁,InnoDB必须打破它。它会选择其中一个事务作为“牺牲者”,将其回滚。选择的标准通常是:
- 影响最小:优先选择持有锁最少的事务(即修改行数最少、或undo log最小的)回滚。
- 或者回滚代价最小的事务。
被选中的事务会被回滚,并释放其持有的所有锁,从而让其他事务得以继续。客户端会收到类似 Deadlock found when trying to get lock; try restarting transaction 的错误。
6.2.4 性能开销
死锁检测在并发极高、锁冲突频繁的场景下,可能会成为性能瓶颈。因为每次等待都要遍历整个等待图。MySQL 5.7及之后,提供了一种优化:对于使用innodb_deadlock_detect参数(默认ON),可以将其关闭。关闭后,系统依靠innodb_lock_wait_timeout来处理死锁。这在某些超高并发场景(如秒杀)下,有时反而能提升性能,但代价是可能频繁出现锁超时。
6.3 经典死锁案例分析
案例1:AB-BA死锁
事务1:UPDATE t SET name='a' WHERE id=1; – 锁住id=1的X锁
事务2:UPDATE t SET name='b' WHERE id=2; – 锁住id=2的X锁
事务1:UPDATE t SET name='a2' WHERE id=2; – 等待事务2释放id=2的锁
事务2:UPDATE t SET name='b2' WHERE id=1; – 等待事务1释放id=1的锁,形成循环。
解法:保证访问资源的顺序一致。例如都先更新id小的,再更新id大的。
案例2:唯一键冲突死锁
这是最容易被忽视的死锁场景。
sql
1 | |
解法:设计业务逻辑,避免并发插入相同唯一键值。
案例3:表锁与行锁的相互作用【强化】
sql
1 | |
在这种情况下,事务2的显式表锁需要等待事务1的所有行锁释放,因为表锁与行锁冲突(X锁与IX锁不兼容,但实际上事务1持有IX锁和大量行X锁,事务2的显式X锁需要等待所有IX锁释放)。这展示了表锁与行锁的层级阻塞关系。
七、总结
InnoDB的锁机制是一个庞大而精密的系统工程。它从硬件原子操作出发,构建了灵活多样的锁类型,包括全局锁、表锁(意向锁、显式表锁)、行锁(记录锁、间隙锁、Next-Key锁)等;
在表锁层面,我们详细剖析了意向锁如何实现多粒度锁的协作,显式表锁(LOCK TABLES)的底层实现,以及元数据锁(MDL)与InnoDB表锁的关系。这些知识对于诊断高并发下的锁竞争至关重要。
理解InnoDB锁的原理,不仅仅是知道有哪些锁,更要了解锁请求是如何被处理的。只有这样,当线上出现突发的锁等待或死锁时,才能透过现象看本质,迅速定位问题根源。无论是优化慢SQL,还是调整事务逻辑,亦或是进行参数调优,都能做到有理有据,胸有成竹。