InnoDB 锁


InnoDB 同时支持行锁和表锁。从是否独占锁的角度上讲,这些锁又可以分为共享锁和排它锁。锁机制,进一步完善了并发下数据一致性的策略。

测试环境

MySQL 版本:8.0.25
隔离模式:默认隔离模式 RR(Repeatable read)
存储引擎:InnoDB

共享锁和排它锁

共享锁和排它锁都是行锁(row-level locking)。

  • 共享锁(S),允许持有该锁的事务读取该行;
  • 排它锁(X),允许持有该锁的事务更新或和删除该行。

如果事务 T1 持有某行 r 的共享锁(S),接着,另外一个事务 T2 请求 r 行锁的处理如下所示:

  • T2 对锁的请求立即被允许,此时,T1 和 T2 都持有锁 r。
  • 如果 T2 请求的是排它锁(X),这个请求无法被立即满足。

如果 T1 持有的是 r 上的互斥锁,其它事务 T2 请求 r 锁时则无法被立即允许。T2 需要等待 T1 释放 r 上的锁。官网的说法

根据在 MySQL 中的实际测试结果,与上述描述基本一致,但是,当 r 行锁为互斥锁时,此时,是可以请求到该行的共享锁的,这种即使可能是源于 MySQL 中的 MVCC 多版本控制机制

多版本并发控制(Multiversion Concurrency Control),每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回;在这时,读写操作之间的冲突就不再需要被关注,而管理和快速挑选数据的版本就成了 MVCC 需要解决的主要问题。来源

如下图示,我们有两个客户端 A 和 B,在 A 客户端上,开启一个事务,并通过 select for update 获取该行的排它锁。此时,在 B 中,我们仍然可以读取该数据的,甚至可以获取该行的共享锁:

如下图示,如果 A 先获取了该行的共享锁,此时,在 B 中仍然可以获取到该行的共享锁,但是无法获取到该行的排它锁。

行锁

在 InnoDB 中,默认是采用了行级锁的。

  • 自动加锁:对于 UPDATE/DELETE/INSERT等语句,InnoDB 会自动给相应行加排它锁。
  • 显式加锁:对于普通 SELETCT 语句,本是没有锁的。但是,可以通过select *** lock in share more 的方式加上共享锁,或通过select *** for update 给相应行加上排它锁。

行锁的实现算法

行级锁的算法实现方式,主要有以下三种,无论是哪种实现方式,其实都是对索引记录进行加锁

行锁算法 锁定内容
Record Lock A record lock is a lock on an index record.
Gap Lock A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record.
Next-Key Lock A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.
记录锁(Record Lock)

记录锁实现了对单个索引的锁定。比如说,我们使用了

1
select * from users where id=30 for update ;

上面的语句,将锁定 id 为 30 的行,以防止其它事务对其 编辑。如果表中没有索引,根据官网的说法,InnoDB 会创建一个隐式的聚集索引,在使用锁的时候可能会锁定这个隐式索引。实际测试,并发生这种情况,而是使用了表级锁。

从上图上我们可以看到,update 语句执行,获取了该行的排他锁,此时,A 事务中,如果通过不需要获取锁的方式,可以看到基于 A 事务开始时间的值,但是,如果 A 事务想要通过排他锁的方式查看该值,会发生超时,因为 B 事务一直没有释放该锁。
之所以在 A 事务中,仍然能够看到值,这是由于 MySQL 中的 MVCC 机制。

间隙锁(Gap Lock)

间隙锁会锁定一定范围内的索引记录,但不包括记录本身。比如:

1
select * from users where id between 10 and 20 for update ;

间隙锁的存在,可以有效解决 幻读 的问题。在一定范围内锁定,防止中途有其它行插入或删除,导致在此范围内查询出来的结果不一样。

Next-Key Lock

记录锁,防止别的事务对加锁的记录修改、编辑;间隙锁防止别的事务进行插入操作;两者结合形成的 Next-Key Lock,共同解决了 RR 级别的幻读问题。

行级锁的优势的弊端

锁的粒度小,因此并发能力强,发生锁冲突的概率低。
但是,其开销比较大,加锁速度慢,且会出现死锁。

表锁

表锁,提供了一种锁定整张表的途径,客户端通过这种方式,在某个事务中拥有了对某张表的唯一访问权。相对于行锁而言,它的锁粒度较大,容易发生碰撞,但是加锁速度块,不会出现死锁。
因此,表锁和行锁在某型情况下其实是形成互补的。
比如,如果事务中需要更新大量的数据,而表又比较大,如果使用行锁,反而会降低效率,且增加了死锁的风险;亦或者在事务中涉及较为复杂的多表关联查询,也可能会引发死锁。这些都是表锁的使用场景。

表锁的加锁/解锁方式

表级锁的加锁,是通过 LOCK TABLE tbl_name 来完成的。

1
2
3
LOCK TABLE keyt READ;   // 加读锁
LOCK TABLE keyt WRITE ; // 加写锁
UNLOCK TABLES; // 释放所有锁

读锁,持有该锁的会话,可以读表,但是不能写表;多个会话可以同时获取读锁,但是就是没有获取到读锁,也不影响对该表的读,但是,它们都不能写

写锁,持有该锁的会话,可以读、写表,这种锁是独占式的,排他的,在锁被释放前,其它会话不能访问该表(阻塞)。

行锁自动升级为表锁

在 InnoDB 中默认使用行锁,但是在某些情况下,会自动由行锁升级为表锁。
首先,SQL 语句中没有使用索引,但是又需要锁时,这时将自动使用表锁。
如果 SQL 中,使用了普通索引,如果这个普通索引没有那么高效(重复性很高),那么它可能会被优化掉,从而导致升级到表锁。

行锁,实际锁的对象不是行,而是按索引锁定,也就是说锁不会定位到某条记录,而是通过限制索引来间接作用到记录。 来源

测试如下所示:

使用另外一张表,做个测试,在未给 userName 增加唯一主键前,和上述发生同样的阻塞事件,但是,为 userName 增加唯一主键后,使用了行级锁。

表锁的优势和弊端

加锁速度块,开销小,不会造成死锁;但是由于锁的粒度较大,容易发生碰撞,在极大多数情况下,没有行锁的并发性高。

意向锁

InnoDB 中,是允许行锁和表锁共存的。而在表锁和行锁之间,其实也存在某些互斥关系,会话不可同时拥有。
意向锁,就是这样一种存在,让行锁和表锁之间的互斥关系的比较变得简单。
意向锁是一种由存储引擎自己维护的用户无法操作的表锁,它不与行级锁冲突,但是会与一般表锁相互斥。在对数据行加共享锁或者排它锁时,引擎会首先请求该表的意向锁,此时,如果你再请求表锁时,表锁会去跟该表的意向锁相比较。

意向锁分为两种:

  • 意向共享锁:当会话请求行的共享锁,就会先去请求其表的意向共享锁,比如:

    1
    select * from keyt where age = 100 lock in share mode;
  • 意向排它锁:当会话请求行的排它锁时,就会先去请求其表的意向排它锁,比如:

    1
    select * from keyt where age =100 for update ;

    || 意向共享锁(IS)| 意向排他锁(IX)|
    |—|—|
    |表级共享锁(S) |兼容| 互斥|
    |表级排他锁(X)| 互斥| 互斥|


参考链接
InnoDB Locking
浅谈数据库并发控制 - 锁和 MVCC
Innodb中的事务隔离级别和锁的关系
LOCK TABLES and UNLOCK TABLES Statements
MySQL 表锁和行锁机制
详解 MySql InnoDB 中意向锁的作用

© 2025 YueGS