当前位置: 首页 > news >正文

【MySQL】第6节|深入理解Mysql事务隔离级别与锁机制

事务及其ACID属性

ACID

  • 原子性(Atomicity) :对数据的修改,要么全都执行,要么全都不执行。
  • 一致性(Consistent) :其他3个特性为实现一致性服务,数据完整可靠,逻辑不会对不上。
  • 隔离性(Isolation) :事务独立执行空间,相互不影响。
  • 持久性(Durable) :事务如果成功提交,必须确保能持久化。

并发事务的问题

更新丢失或脏写:最后的更新覆盖了由其他事务所做的更新。

脏读:事务A读取到了事务B已经修改但尚未提交的数据。

不可重复读:一个事务内相同语句执行多次结果不一致。

幻读:事务A读取到了事务B提交的新增数据。

事务隔离级别

隔离级别

脏读

不可重复读

幻读 

读未提交

可能

可能

可能

读已提交

不可能

可能

可能

可重复读

不可能

不可能

可能

可串行化

不可能

不可能

不可能

锁机制

锁的分类

从性能分:乐观锁,悲观锁

操作粒度:表锁,行锁

操作类型:

读锁(共享锁,S锁),select * from T where id=1 lock in share mode

写锁(排它锁,X锁),select * from T where id=1 for update

意向锁:mysql内部用,用于标记表没有没行锁,从而判断能不能加表锁,提高加锁效率,因为不用每次逐行判断有没有行锁

表锁

每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;一般用在整表数据迁移的场景。

行锁

每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。需要InnoDB支持行锁和事务。

间隙锁(Gap Lock)

在 MySQL 中,间隙锁(Gap Lock)是 InnoDB 存储引擎在 可重复读(REPEATABLE READ) 隔离级别下为解决 幻读问题 而引入的一种锁机制。当满足以下条件时,间隙锁会被触发:

一、触发间隙锁的必要条件

  1. 隔离级别为 REPEATABLE READ(默认)

间隙锁仅在该隔离级别下生效。若隔离级别为 读已提交(READ COMMITTED),间隙锁会被禁用,转而使用 记录锁(Record Lock)

  1. 使用索引进行查询

间隙锁必须通过索引条件触发。若查询未使用索引(如全表扫描),InnoDB 会锁定整个表,而非仅间隙。

  1. 当前读(显式锁或写操作)

间隙锁仅在 当前读(如 SELECT ... FOR UPDATEUPDATEDELETE)时触发。普通的 SELECT(快照读)不会触发间隙锁。

二、常见触发场景

1. 范围查询(Range Query)
-- 假设索引为 idx_age (age),且存在记录 age=20, 25, 30
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;-- 触发间隙锁的范围:
-- (负无穷, 20], (20, 25], (25, 30], (30, 正无穷)
  • 作用:阻止其他事务在该范围内插入新记录(如 age=23),避免幻读。
2. 唯一索引的等值查询(不存在的记录)
-- 假设唯一索引为 idx_id (id),且不存在 id=100 的记录
SELECT * FROM users WHERE id = 100 FOR UPDATE;-- 触发间隙锁:
-- 若表中存在 id=99 和 id=101,则锁定 (99, 101)
  • 作用:防止其他事务插入 id=100 的记录,保证唯一性。
3. 非唯一索引的等值查询
-- 假设非唯一索引为 idx_name (name),存在多条 name='Alice' 的记录
SELECT * FROM users WHERE name = 'Alice' FOR UPDATE;-- 触发间隙锁:
-- 锁定所有 name='Alice' 记录的前后间隙
  • 作用:阻止插入 name='Alice' 的新记录,避免幻读。
4. 插入意向锁(Insert Intention Lock)冲突

当多个事务尝试在同一间隙插入不同值时,会触发插入意向锁,本质是间隙锁的一种:

-- 事务 T1
INSERT INTO users (age) VALUES (23);  -- 尝试插入间隙 (20, 25)-- 事务 T2
INSERT INTO users (age) VALUES (24);  -- 尝试插入同一间隙
  • 结果:T2 需等待 T1 提交或回滚,否则会被阻塞。

三、间隙锁的危害与优化

1. 可能导致的问题
  • 死锁:多个事务在同一间隙交叉加锁时可能导致死锁。
  • 性能下降:锁定范围过大,影响并发插入性能。
2. 优化建议
  • 降低隔离级别:使用 READ COMMITTED 隔离级别,禁用间隙锁。
  • 优化查询条件:避免范围查询,精确匹配记录。
  • 添加索引:确保查询条件使用索引,减少锁的范围。

四、验证间隙锁的存在

通过 SHOW ENGINE INNODB STATUS 查看当前锁信息:

SHOW ENGINE INNODB STATUS\G;-- 输出中可能看到类似信息:
RECORD LOCKS space id 10 page no 5 n bits 80 index PRIMARY of table `test`.`users` 
trx id 12345 lock_mode X locks gap before rec

总结

间隙锁主要在 可重复读隔离级别下的当前读操作中 触发,目的是防止幻读。常见于 范围查询、唯一索引的等值查询(查不存在的记录)、非唯一索引的等值查询 等场景。合理控制隔离级别和查询条件,可减少间隙锁带来的负面影响。

临键锁(Next-key Locks)

在 MySQL 的 InnoDB 存储引擎中,临键锁(Next-Key Locks) 是一种组合锁,它结合了 记录锁(Record Lock) 和 间隙锁(Gap Lock) 的功能,用于在 可重复读(REPEATABLE READ)隔离级别 下解决幻读问题。临键锁的核心作用是锁定一个 左闭右开区间(即前一个记录的间隙到当前记录之间的范围),确保在该范围内的数据不会被其他事务插入、修改或删除,从而避免幻读。

一、临键锁的本质:记录锁 + 间隙锁

  • 记录锁(Record Lock):锁定索引中的单个记录(如 id=10)。
  • 间隙锁(Gap Lock):锁定索引记录之间的间隙(如 (10, 20))。
  • 临键锁(Next-Key Lock):两者的组合,锁定 前一个间隙到当前记录的左闭右开区间(如 (5, 10],假设前一个记录是 id=5)。

二、临键锁的触发条件

  1. 隔离级别为可重复读(REPEATABLE READ,默认)

临键锁仅在该隔离级别下生效,其他隔离级别(如读已提交)会禁用间隙锁,仅使用记录锁。

  1. 通过索引访问数据

必须通过索引条件查询(如 WHERE id=10 或 WHERE age>20),若未使用索引(全表扫描),会退化为表锁。

  1. 当前读操作(加锁操作)

如 SELECT ... FOR UPDATEUPDATEDELETE 等会触发当前读,普通 SELECT(快照读)不会触发临键锁。

三、临键锁的锁定范围(示例)

假设表中有索引字段 id,值为 10、15、20、25,则索引间隙为:

(-∞, 10)、(10, 15)、(15, 20)、(20, 25)、(25, +∞)

场景 1:等值查询(存在记录)
SELECT * FROM table WHERE id = 15 FOR UPDATE;
  • 锁定范围:临键锁会锁定 (10, 15] 区间(前一个间隙 (10,15) + 记录 id=15)。
    • 阻止其他事务插入 (10,15) 间隙的数据(如 12、13)。
    • 阻止修改或删除 id=15 的记录。
场景 2:等值查询(不存在记录)
SELECT * FROM table WHERE id = 12 FOR UPDATE;
  • 锁定范围:若 id=12 不存在,但相邻记录为 id=10 和 id=15,则锁定 (10, 15) 间隙(纯间隙锁)。
    • 阻止其他事务插入 (10,15) 区间的数据(如 12、13)。
场景 3:范围查询
SELECT * FROM table WHERE id BETWEEN 10 AND 20 FOR UPDATE;
  • 锁定范围
    • 记录锁:id=10、15、20
    • 间隙锁:(-∞,10)(10,15)(15,20)(20,25)
    • 组合为临键锁:(-∞,10](10,15](15,20](20,25)(注意最后一个间隙为纯间隙锁,因为 25 不在范围内)。
    • 阻止插入 (-∞,10)(10,15)(15,20) 区间的数据,以及修改 id=10、15、20 的记录。

四、临键锁与幻读的关系

  • 幻读的定义:事务 T1 读取某个范围的数据后,事务 T2 在该范围内插入新数据,T1 再次读取时发现新增数据,如同“幻觉”。
  • 临键锁的作用:通过锁定记录及其间隙,阻止其他事务在范围内插入数据,从而避免幻读。

五、临键锁的优化与注意事项

1. 可能的问题
  • 锁范围过大:大范围查询可能锁定大量间隙,导致并发性能下降。
  • 死锁风险:多个事务交叉锁定相邻间隙时可能引发死锁。
2. 优化方法
  • 降低隔离级别:改用 READ COMMITTED,此时 InnoDB 会禁用间隙锁,仅使用记录锁(需配合 innodb_locks_unsafe_for_binlog=1 参数)。
  • 缩小查询范围:避免不必要的范围查询,尽量使用精确条件(如 id=10 而非 id>5)。
  • 合理设计索引:确保查询条件使用索引,避免全表扫描导致表锁。

六、如何验证临键锁的存在?

通过 SHOW ENGINE INNODB STATUS 命令查看锁信息,临键锁会显示为 lock_mode X next-key

SHOW ENGINE INNODB STATUS\G;-- 输出示例:
RECORD LOCKS space id 123 page no 4 n bits 72 index idx_id of table `test`.`t`
trx id 10001 lock_mode X next-key lock on id in (10,15]

总结

  • 临键锁 = 记录锁 + 间隙锁,锁定左闭右开区间,防止幻读。
  • 仅在 可重复读隔离级别 和 当前读操作 中生效,依赖索引触发。
  • 优化时需平衡隔离级别、查询条件和索引设计,避免锁范围过大影响性能。

死锁

在 MySQL 的 InnoDB 存储引擎中,死锁通常发生在多个事务互相等待对方释放锁的场景下。以下是一个典型的 基于临键锁(Next-Key Locks) 的死锁案例,涉及索引范围查询和事务交叉锁定:

场景背景

表结构:

CREATE TABLE `order` (`id` INT PRIMARY KEY AUTO_INCREMENT,`amount` DECIMAL(10,2),INDEX `idx_amount` (`amount`)
);-- 表中已有数据(按 amount 排序):
-- amount: 100, 200, 300

死锁过程演示

事务 T1(用户 A 操作)
  1. 步骤 1:查询并锁定 amount > 150 的记录(范围查询,触发临键锁)
BEGIN;
SELECT * FROM `order` WHERE amount > 150 FOR UPDATE;
    • 锁定范围
      • 索引 idx_amount 中,amount=200 和 300 被记录锁锁定。
      • 间隙锁锁定区间:(100, 200)(200, 300)(300, +∞)
      • 临键锁组合为:(100, 200](200, 300](300, +∞)(左闭右开)。
  1. 步骤 2:尝试更新 amount=100 的记录(需锁定 (−∞, 100] 区间)
UPDATE `order` SET amount = 101 WHERE amount = 100;
    • 等待原因amount=100 对应的临键锁为 (−∞, 100],但该区间已被事务 T2 锁定,T1 进入阻塞。
事务 T2(用户 B 操作)
  1. 步骤 1:查询并锁定 amount < 250 的记录(范围查询,触发临键锁)
BEGIN;
SELECT * FROM `order` WHERE amount < 250 FOR UPDATE;
    • 锁定范围
      • 索引 idx_amount 中,amount=100 和 200 被记录锁锁定。
      • 间隙锁锁定区间:(−∞, 100)(100, 200)(200, 300)(因 250 介于 200 和 300 之间)。
      • 临键锁组合为:(−∞, 100](100, 200](200, 250)(左闭右开,最后一个间隙为纯间隙锁)。
  1. 步骤 2:尝试更新 amount=300 的记录(需锁定 (250, 300] 区间)
UPDATE `order` SET amount = 301 WHERE amount = 300;
    • 等待原因amount=300 对应的临键锁为 (250, 300],但该区间已被事务 T1 锁定,T2 进入阻塞。

死锁形成

  • T1 持有锁(100, 200](200, 300](300, +∞),等待 (−∞, 100]
  • T2 持有锁(−∞, 100](100, 200],等待 (250, 300](属于 T1 锁定的 (200, 300] 区间的一部分)。
  • 循环等待:T1 等待 T2 释放 (−∞, 100],T2 等待 T1 释放 (250, 300],形成死锁。

死锁日志验证

通过 SHOW ENGINE INNODB STATUS 查看死锁日志,会显示类似以下内容:

------------------------
LATEST DEADLOCK DETECTION
------------------------
2025-05-23 14:30:00
*** (1) TRANSACTION:
TRANSACTION 10001, ACTIVE 60 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), 1 next-key lock(s)
MySQL thread id 10, OS thread handle 12345, query id 1000 user@localhost updating
UPDATE `order` SET amount = 101 WHERE amount = 100
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 124 page no 3 n bits 80 index idx_amount of table `test`.`order`
lock_mode X next-key lock waiting on id in (-∞,100]
*** (2) TRANSACTION:
TRANSACTION 10002, ACTIVE 60 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), 1 next-key lock(s)
MySQL thread id 11, OS thread handle 12346, query id 1001 user@localhost updating
UPDATE `order` SET amount = 301 WHERE amount = 300
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 124 page no 5 n bits 72 index idx_amount of table `test`.`order`
lock_mode X next-key lock waiting on id in (250,300]
*** WE ROLL BACK TRANSACTION (2)
  • InnoDB 会自动检测死锁并回滚其中一个事务(通常是成本较低的事务)以释放锁。

死锁的常见原因与避免方法

原因总结
  1. 临键锁的范围锁定:范围查询导致锁定大量间隙,交叉请求时易引发循环等待。
  2. 事务顺序不一致:不同事务以相反顺序请求锁(如 T1 先锁 A 再锁 B,T2 先锁 B 再锁 A)。
  3. 索引缺失:全表扫描导致表锁,扩大锁定范围。
避免方法
  1. 按固定顺序访问资源:确保所有事务以相同顺序操作数据(如按主键升序锁定)。
  2. 缩小事务范围:减少事务持锁时间,避免在事务中执行无关操作。
  3. 降低隔离级别:改用 READ COMMITTED(需配合参数禁用间隙锁)。
  4. 优化索引:确保查询使用索引,避免全表扫描和大范围锁。
  5. 设置死锁超时:通过 innodb_lock_wait_timeout 参数调整死锁检测超时时间(默认 50 秒)。

总结

上述场景中,两个事务因范围查询触发临键锁,交叉锁定对方所需的间隙和记录,导致死锁。理解临键锁的锁定范围和事务执行顺序是避免死锁的关键,合理设计索引和事务逻辑可有效降低死锁风险。

锁优化

  • 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
  • 合理设计索引,尽量缩小锁的范围
  • 尽可能减少检索条件范围,避免间隙锁
  • 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
  • 尽可能低级别事务隔离

系统表

-- 查看事务
select * from INFORMATION_SCHEMA.INNODB_TRX;
-- 查看锁
select * from INFORMATION_SCHEMA.INNODB_LOCKS;
-- 查看锁等待
select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;-- 释放锁,trx_mysql_thread_id可以从INNODB_TRX表里查看到
kill trx_mysql_thread_id-- 查看锁等待详细信息
show engine innodb status\G; 

相关文章:

  • 智慧应急指挥调度系统:构建城市安全“防护罩”
  • 企业知识管理面临的挑战与飞书知识问答的解决方案
  • 软件中级考试之软件设计师下午篇ER图做题方法总结
  • Android帧抢占协议技术剖析:触摸事件与UI绘制的智能调度优化方案
  • 智警杯备赛--数据应用技术1
  • 嵌入式系统C语言编程常用设计模式---参数表驱动设计
  • 缓存穿透 击穿 雪崩
  • 【分布式文件系统】FastDFS
  • 基于非线性规划的电动汽车充电站最优布局
  • YOLOv11改进 | Conv/卷积篇 | 2024 ECCV最新大感受野的小波卷积WTConv助力YOLOv11有效涨点
  • Python 和 matplotlib 保存图像时,确保图像的分辨率和像素符合特定要求(如 64x64),批量保存 不溢出内存
  • 国产化Word处理控件Spire.Doc教程:使用 Python 创建 Word 文档的详细指南
  • maven添加自己下载的jar包到本地仓库
  • 「金融证券行业」 如何搭建自己的研发智能管理体系?
  • 【人工智能】低代码-模版引擎
  • 二十三、面向对象底层逻辑-BeanDefinitionParser接口设计哲学
  • 现代生活下的创新健康养生之道
  • Idea 配合 devtools 依赖 实现热部署
  • VSCode+EIDE通过KeilC51编译,使VSCode+EIDE“支持”C和ASM混编
  • Idea如果有参数,怎么debug
  • vue 做pc网站可以吗/外链收录网站
  • 汽车行业网站建设/百度搜索网
  • 布吉做棋牌网站建设哪家便宜/如何做推广最有效果
  • wordpress 大站点/苏州搜索引擎排名优化商家
  • 我自己做个网站怎么做/找关键词的方法与技巧
  • 文成做网站/磁力库