MySQL 锁


一、锁的核心作用:解决并发冲突

锁是计算机协调多个进程或线程并发访问某一资源的机制。

在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。

MySQL 的锁机制是保障数据一致性和并发性能的核心工具。它通过不同粒度、不同模式的锁,协调多个事务对数据的访问,避免脏读、幻读、丢失更新等问题。

数据库是多用户共享的资源,当多个事务(用户操作)同时访问同一数据时,可能引发:

  • 脏读:事务A读取了事务B未提交的修改(B后回滚,A读到无效数据)。
  • 幻读:事务A两次查询同一范围,结果行数不同(事务B插入了新数据)。
  • 丢失更新:事务A和事务B同时修改同一数据,后提交的覆盖先提交的。

锁的本质是控制对数据的访问权限,通过“互斥”或“共享”规则,确保事务的隔离性(Isolation)和一致性(Consistency)。


查看锁

select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from
performance_schema.data_locks;

要其他手动开启的事务加锁之后,才能查看的到。

图片

二、锁的分类:按粒度、模式、实现维度拆解

image-20240905151233838

(一)按锁定范围(粒度)分类:从粗到细

锁的粒度越粗,管理开销越小,但并发度越低;粒度越细,管理开销越大,但并发度越高。

锁类型 锁定范围 核心特性 典型场景
全局锁 整个数据库(所有表) 最强锁,阻塞全库写操作;通过 FLUSH TABLES WITH READ LOCK 加读锁(写锁需隐式触发) 全库逻辑备份(如 mysqldump
表锁 单个表 开销小、加锁快;读锁(S)允许并发读,互斥写锁(X);写锁阻塞所有读写 MyISAM 引擎(读多写少,如日志表)
行锁 表中某一行(依赖索引) 细粒度锁,支持高并发;InnoDB 核心机制,通过索引定位行;无索引时退化为表锁 InnoDB 高并发事务(如订单、账户)
页锁(淘汰) 数据页(多个行) 粒度介于表锁与行锁之间,现仅历史引擎(BDB)支持,几乎淘汰

1.全局锁

在 MySQL 中锁按颗粒度分为三类

  • 全局锁: 锁定数据库中的所有表。
  • 表级锁:每次操作锁住整张表。
  • 行级锁:每次操作锁住对应的行数据

介绍

全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的 DML 的写语句,DDL 语句,已经更新操作的事务提交语句都将被阻塞。

其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。

语法

1.终端1

#登录到MySQL
flush tables with read lock ;

2.终端2

#不要登录到MySQL,而是在MySQL的bin目录下的mysqldump
mysqldump -uroot –proot123 test > test.sql

3.回到终端1

unlock tables; # 释放全局锁

2 表级锁

表级锁,就是锁整张表。

2.1 介绍

  • 表锁
  • 元数据锁(meta data lock,MDL)
  • 意向锁

2.2 表锁

表共享锁

# 加锁
lock tables table_name read;
# 解锁
unlock tables;

所有事务只能读不可写

表独占写锁

# 加锁
lock tables table_name write;
# 解锁
unlock tables;

只有事务自己可读可写,其他不可读亦不可写

3 元数据锁

MDL加锁过程是系统自动控制,无需显式使用,在访问一张表的时候会自动加上。MDL锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。**为了避免DML与DDL冲突,保证读写的正确性。**  ==说白了,就是修改表结构的锁==

3.1MDL 读锁

  • 在 MySQL5.5 中引入了 MDL,当对一张表进行增删改查的时候,加 MDL 读锁 (共享);
  • 就是,事务有操作时,其他事务不能对表结构就行修改。

3.2MDL 写锁

  • 当对表结构进行变更操作的时候,加 MDL 写锁 (排他)。
  • 当对表结构就行修改时,其他事务不能读写。

4 意向锁

为了解决行锁与表锁的冲突而出现,意向锁本质是表锁,用来标识该表的行锁情况,避免其他事务要遍历表来检查行锁。

说说过程

1.当事务 A 进行增删改查 (查需要手动加共享锁) 的时候,会先对需要操作的行进行加锁。并且,InnoDB 会对整张表加一个意向锁。

2.其他事务想加表锁,不再需要逐行扫描行锁了,直接根据意向锁来解决锁是否可以兼容加锁即可,极大提高了效率。

4.1 意向锁分类

  • 意向共享锁 (IS):

    • 由语句 select … lock in share mode 添加 。
    • 与表锁共享锁 (read) 兼容,与表锁排他锁 (write) 互斥。
    • 手动添加
  • 意向排他锁 (IX):

  • 由 insert、update、delete、select…for update 添加 。与表锁共享锁 (read) 及排他锁 (write) 都互斥,意向锁之间不会互斥。
  • 自动添加

5. 行级锁

行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在 InnoDB 存储引擎中。

5.1 行锁分类

InnoDB 的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级锁,主要分为以下三类:

  • 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行 update 和 delete。在 RC、RR 隔离级别下都支持。

图片

  • 间隙锁(Gap Lock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行 insert,产生幻读。在 RR 隔离级别下都支持。

图片

  • 临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙 Gap。在 RR 隔离级别下支持。

图片

5.2 InnoDB 的行锁

共享锁(S) - 允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁,但是可以自己执行增删改来把行升级成排它锁。

排他锁(X) - 允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。即,自己可以读写,其他事务只可以读 - 注意: 要与表锁区分,表锁的写锁,其他事务既不可读也不可写。

图片

常见的 SQL 行锁情况:

SQL 行锁类型 说明
INSERT … 排他锁 自动加锁
UPDATE … 排他锁 自动加锁
DELETE … 排他锁 自动加锁
SELECT(正常) 不加任何锁
SELECT … LOCK IN SHARE MODE 共享锁 之后执行增删改,则会自动升级成排它锁。
SELECT … FOR UPDATE 排他锁 需要手动在 SELECT 之后加 FOR UPDATE
  • 默认情况下,InnoDB 的隔离级别是 REPEATABLE READ 即可重复读,另外使用了 next-key 锁 (间隙锁) 进行搜索和索引扫描,以防止幻读 。

警告

  • 因为 InnoDB 的行锁是针对索引的,所以如果 where 字段中没有索引或者索引失效,那么将升级成表锁!!
  • 不管 where 用的是二级还是主键索引,最后统统优化成主键索引!

(二)按锁的模式(兼容性)分类:共享与排他

锁的模式定义了“是否允许其他事务同时加锁”,核心是共享锁(S)排他锁(X),以及辅助的意向锁(IS/IX)

锁模式 含义 兼容性
共享锁(S) 允许事务读数据(如 SELECT ... LOCK IN SHARE MODE 与 S 锁兼容(多事务可同时读);与 X 锁互斥(读时不能写)
排他锁(X) 允许事务写数据(如 INSERT/UPDATE/DELETE 隐式加锁,或 SELECT ... FOR UPDATE 与所有锁互斥(写时不能读/写)
意向共享锁(IS) 表级锁,表示“事务意图对表中某些行加 S 锁”(读前预告) 与 IS、IX 锁兼容;与 X 锁互斥
意向排他锁(IX) 表级锁,表示“事务意图对表中某些行加 X 锁”(写前预告) 与 IS 锁兼容;与 IX、S、X 锁互斥

(三)InnoDB 行锁的增强:防幻读与高并发

InnoDB 为支持“可重复读”隔离级别(MySQL 默认),在行锁基础上扩展了间隙锁临键锁,彻底解决幻读问题。

锁类型 定义 作用
记录锁(Record) 锁定索引上的单个记录(如 id=10 基础行锁,确保单条记录的读写互斥
间隙锁(Gap) 锁定索引记录之间的间隙(如 id 在 10 和 20 之间的间隙) 阻止其他事务在间隙插入数据,避免幻读(两次查询行数不一致)
临键锁(Next-Key) 记录锁 + 间隙锁的组合(左开右闭区间,如 (10,20] InnoDB 可重复读的默认策略,锁定记录及其前间隙,彻底防止幻读
插入意向锁 插入数据时,在间隙上申请的轻量级锁(与同向间隙锁兼容) 允许同间隙并发插入(如多个事务同时插入 id=15),提升插入效率

记录锁 & 间隙锁 & 临键锁

默认情况下,InnoDB 在 REPEATABLE READ 事务隔离级别运行,为了好好理解锁的应用场景我们分情况讨论。

记录锁: - 主键或唯一索引 等值 命中触发

间隙锁: - 使用索引,不论等值/范围,未命中

锁类型 命中时机 范围 备注
记录锁 主键或者唯一索引并等值,并命中 被查出来的记录本身
间隙锁 使用索引,不论等值 / 范围,未命中 左开右开 间隙锁,就只有间隙
临键锁 使用索引,范围查询,并命中 左闭右开 记录锁 + 临键锁的结合

图片

  • 间隙锁与临键锁有时候很混乱,直接给例子说明(后缀全是 lock in share mode)

  • 命中记录时

select ... where age = 5 #锁区间:  [1,5)  [5]  [5,10),即[1,10)

其实这我不理解,为什么大家都说是左开右闭,但是我反复试验,age=1 的记录 (id 自增) 是无论如何都无法插入,我怀疑是临键锁。

或者说,间隙锁不会单独出现,它虽然是左开右开,但是会与记录锁共同作用成临键锁。

select ... where age > 5 #锁区间:  [5,+∞)
select ... where age = 15 #锁区间:  [10,15)  [15]   [15,+∞)
  • 记录不存在时:
select ... where age > 15 #锁区间:  [15,+∞)
select ... where age > 1000 #锁区间:  [1000,+∞)
select ... where age = 1000 #锁区间:  [1000,+∞)

(四)应用层锁策略:悲观锁 vs 乐观锁

这是两种解决并发冲突的思想模型,可在数据库或应用层实现。

策略 核心思想 实现方式 适用场景
悲观锁 假设冲突高频,提前加锁(“先占座再改”) 数据库层面:SELECT ... FOR UPDATE(X锁)、SELECT ... LOCK IN SHARE MODE(S锁) 写频繁、强一致性(如库存扣减)
乐观锁 假设冲突低频,不加锁,更新时校验(“先改再检查”) 应用层:版本号机制(version 字段校验)、CAS(INSERT ... ON DUPLICATE KEY UPDATE 读多写少、允许重试(如浏览量统计)

三、锁的兼容性矩阵:哪些锁可以共存?

锁的兼容性决定了多事务能否同时访问同一数据。以下是表级锁与行级锁的兼容规则(✅=兼容,❌=互斥):

现有锁 \ 请求锁 S锁(共享) X锁(排他) IS锁(意向共享) IX锁(意向排他)
S锁(表/行)
X锁(表/行)
IS锁(表级)
IX锁(表级)

四、锁的典型问题与优化

1. 死锁:互相等待的“死循环”

  • 原因:事务A持有行1的X锁,请求行2的X锁;事务B持有行2的X锁,请求行1的X锁,形成循环等待。
  • 解决
  • InnoDB 自动检测死锁,回滚其中一个事务(通过 innodb_deadlock_detect=ON 控制)。
  • 优化:按固定顺序加锁(如按主键排序)、缩短事务时长、减少锁持有时间。

MySQL中的死锁(Deadlock)是指两个或多个事务在执行过程中由于相互持有对方所需的资源而造成的一种僵局状态。在这种状态下,事务无法继续执行,系统需要进行干预来解决这个问题。

以下是MySQL死锁的基本概念、检测、分析和解决方案:

1.死锁的概念

死锁通常发生在以下情况下:

  • 事务A持有锁资源1,并请求锁资源2。
  • 事务B持有锁资源2,并请求锁资源1。
  • 由于相互持有的锁资源,两个事务都无法继续执行,从而产生死锁。

2. 死锁的检测

MySQL自动检测死锁,并在发生死锁时回滚其中一个事务。你可以使用以下方法查看死锁信息:

查看InnoDB死锁日志:

  • 在MySQL日志中,死锁信息通常记录在InnoDB引擎的错误日志中。你可以通过以下命令查看日志文件:
SHOW ENGINE INNODB STATUS;

该命令会返回InnoDB存储引擎的状态信息,其中包括死锁的详细信息。

3. 分析死锁

在查看 SHOW ENGINE INNODB STATUS 的输出时,找到“LATEST DETECTED DEADLOCK”部分,这里会显示最近检测到的死锁信息。关键字段包括:

  • TRANSACTIONS: 包含参与死锁的事务的详细信息。
  • LOCKS: 发生死锁的锁资源。
  • WAITING FOR: 当前事务正在等待的资源。
  • HELD BY: 当前事务持有的资源。

通过这些信息,可以确定哪个事务被回滚了,以及死锁的原因。

4. 解决死锁

解决死锁的方法主要包括以下几种:

4.1. 设计事务

  • 减少事务的持续时间: 尽量将事务的执行时间缩短,减少持有锁的时间。
  • 按照相同的顺序访问资源: 确保所有事务按照相同的顺序访问锁资源,避免循环依赖。
  • 使用合理的锁粒度: 尽量避免对整个表进行锁定,尽量使用行级锁。

4.2. 处理死锁

  • 重试机制: 在应用程序中捕获死锁异常(通常是错误代码1213),并实现重试机制。重试机制可以在事务被回滚后自动重试,以期获得成功。

示例(伪代码)

while True:
    try:
        # 执行数据库操作
        break
    except DeadlockException:
        # 捕获死锁异常,重试
        continue
  • 使用合理的隔离级别: 在事务中选择适当的隔离级别。通常,使用较低的隔离级别可以减少死锁的概率,但可能会增加脏读等其他问题。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

5. 实践示例

假设有两个事务如下:

事务A:

START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 等待
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT;

事务B:

START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 2;
-- 等待
UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;
COMMIT;

如果事务A在执行第一个UPDATE时持有了锁,并且事务B在执行第一个UPDATE时也持有了锁,而两个事务都在等待对方释放锁,那么就会发生死锁。

通过合理设计事务及应用上述解决方案,你可以有效地减少和解决MySQL中的死锁问题。

2. 索引失效导致表锁

  • 现象:行锁依赖索引定位行,若查询条件无索引(如 WHERE name='张三'name 无索引),InnoDB 无法定位具体行,退化为表锁。
  • 优化:通过 EXPLAIN 分析 SQL,确保查询条件使用索引(如为 name 字段添加索引)。

3. 间隙锁影响插入性能

  • 现象:范围查询(如 WHERE id>10)触发间隙锁,锁定大量间隙,阻塞其他事务插入。
  • 优化
  • 降低隔离级别(如使用“读已提交”,InnoDB 在此级别默认禁用间隙锁)。
  • 缩小查询范围(如 WHERE id BETWEEN 10 AND 20WHERE id>10 锁范围更小)。

4. 全局锁阻塞业务

  • 现象FLUSH TABLES WITH READ LOCK 加全局读锁期间,所有写操作(INSERT/UPDATE/DELETE)被阻塞。
  • 优化
  • 对 InnoDB 表,使用热备份工具(如 xtrabackup),利用事务的一致性视图,无需加全局锁。
  • 仅在备份 MyISAM 表或需要全库一致性快照时使用全局锁。

五、存储引擎与锁支持对比

不同引擎对锁的支持差异直接影响其适用场景:

引擎 表锁 行锁 意向锁 间隙锁/临键锁 事务支持 典型场景
MyISAM 读多写少、低并发(如日志表)
InnoDB 高并发事务(如订单、账户)
Memory 临时数据(如缓存)

六、总结:锁的选择与最佳实践

MySQL 锁机制的核心是平衡并发性能与数据一致性,选择锁策略时需结合业务场景:

  • 高并发事务:优先 InnoDB 行锁,确保索引有效(避免表锁),合理设置隔离级别(读已提交减少间隙锁)。
  • 低并发/读多写少:MyISAM 表锁或 InnoDB 表锁(如日志表)。
  • 强一致性场景:悲观锁(SELECT ... FOR UPDATE),确保写操作互斥(如库存扣减)。
  • 读多写少场景:乐观锁(版本号机制),减少锁竞争(如浏览量统计)。
  • 全库备份:避免全局锁,用热备份工具(如 xtrabackup)。

通过理解锁的类型、兼容性及优化方法,可精准解决并发问题,让 MySQL 在高并发场景下保持稳定高效。