mysql 锁定读和非锁定读

在 MySQL(特别是 InnoDB 引擎)中,‌锁定读(Locking Read)‌和‌非锁定读(Non-Locking Read)‌是处理并发事务时两种截然不同的读取策略。它们的核心区别在于读取数据时是否会对行或索引加锁,从而影响其他事务的写入操作。

一、非锁定读(Non-Locking Read)

这是 MySQL ‌默认‌的读取行为,也是大多数 SELECT 语句的执行方式。

1. 核心机制:MVCC

非锁定读依赖于 ‌多版本并发控制(MVCC, Multi-Version Concurrency Control)‌。

当事务执行普通 SELECT 时,InnoDB 不会给数据行加任何锁。

它通过读取数据的‌历史版本快照(Snapshot)‌来保证一致性。

具体读取哪个版本取决于当前的事务隔离级别:

‌读已提交(RC)‌:读取该行最新已提交的版本。

‌可重复读(RR)‌:读取事务启动时刻生成的快照版本。

2. 特点

‌高并发‌:读操作不会阻塞写操作,写操作也不会阻塞读操作(除非涉及DDL等元数据锁)。

‌无等待‌:不需要等待其他事务释放锁。

‌潜在问题‌:在 RC 级别下可能出现"不可重复读";在 RR 级别下虽然解决了不可重复读,但在特定场景下(如范围查询插入新数据)仍可能遇到幻读(尽管 InnoDB 通过 Next-Key Lock 尽量规避,但普通 SELECT 本身不加锁,无法阻止其他事务插入新行,只是自身看不到而已)。

3. 适用场景

绝大多数业务查询场景。

对数据实时性要求不高,或者能容忍短暂不一致的场景。

高并发读取场景。

二、锁定读(Locking Read)

锁定读是指显式地在 SELECT 语句中加入锁提示,强制数据库在读取数据时获取锁,从而阻塞其他事务的修改或读取。

1. 两种主要类型

语法 锁类型 作用 阻塞情况
SELECT ... FOR SHARE (MySQL 8.0+) LOCK IN SHARE MODE (旧版) ‌共享锁 (S锁)‌ 允许其他事务读取该行,但‌禁止其他事务修改或删除‌该行。 其他事务的写操作会被阻塞,直到当前事务提交。
SELECT ... FOR UPDATE ‌排他锁 (X锁)‌ ‌禁止其他事务读取(锁定读)、修改或删除‌该行。 其他事务的读写操作(锁定读)都会被阻塞。

2. 核心机制

锁定读会绕过 MVCC 的快照读取,直接读取‌最新已提交的数据‌,并立即对该数据行加上相应的锁。

如果其他事务已经持有了冲突的锁,当前事务会进入‌等待状态‌,直到锁释放或超时。

3. 适用场景

‌数据一致性要求极高‌:需要确保在读取后、更新前,数据不被其他事务修改。

‌业务逻辑依赖当前值‌:例如"检查库存是否充足,若充足则扣减",必须使用 FOR UPDATE 防止超卖。

‌外键约束检查‌:InnoDB 在检查外键约束时,会自动对引用的父表行加共享锁。

‌去重插入‌:在插入前先查询是否存在,若存在则更新,需加锁防止并发插入重复数据。

三、对比总结

维度 非锁定读 (普通 SELECT) 锁定读 (FOR SHARE / FOR UPDATE)
‌是否加锁‌ 是 (S锁或X锁)
‌底层技术‌ MVCC (快照读) 当前读 (Current Read) + 锁机制
‌并发性能‌ 高,读写互不阻塞 低,存在锁竞争和等待
‌数据实时性‌ 可能读到旧版本 (取决于隔离级别) 总是读到最新已提交数据
‌阻塞其他事务‌ 不阻塞 阻塞其他事务的写 (S/X) 或读写 (X)
‌典型用途‌ 日常查询、报表统计 转账、扣库存、唯一性校验

四、实战示例

1. 非锁定读(默认)

java 复制代码
-- 事务 A
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; 
-- 此时事务 B 可以修改 id=1 的 balance,事务 A 不受影响,且看到的是快照数据
COMMIT;

2. 锁定读(排他锁)

java 复制代码
-- 事务 A
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 此时事务 B 如果想执行 UPDATE accounts SET balance=... WHERE id=1; 
-- 或者 SELECT ... FOR UPDATE,事务 B 会被阻塞,直到事务 A 提交或回滚
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

3,举例说明:

1、场景设定

假设有一张商品表 products,其中 id=1 的商品库存 stock 为 ‌10‌。

‌事务 A‌:查询库存并准备扣减。

‌事务 B‌:试图修改同一商品的库存。

2、非锁定读示例(普通 SELECT)

2.1执行流程

时间 事务 A (隔离级别 RR) 事务 B 状态说明
T1 START TRANSACTION; 事务 A 开启
T2 SELECT stock FROM products WHERE id=1; ‌非锁定读‌:读取快照,不加锁
T3 UPDATE products SET stock=5 WHERE id=1; ‌成功‌:因为 A 没加锁,B 可以直接修改并提交
T4 COMMIT; 事务 B 提交,库存变为 5
T5 SELECT stock FROM products WHERE id=1; ‌仍返回 10‌:RR 级别下,A 看到的是 T1 时刻的快照
T6 UPDATE products SET stock=stock-1 WHERE id=1; ‌成功‌:A 基于快照值 10 计算,最终库存变为 9
2.2. 结果分析
‌问题‌:发生了‌丢失更新‌。事务 B 将库存改为 5,但事务 A unaware,基于旧值 10 扣减后变成 9,导致 B 的修改被覆盖。
‌特点‌:读写互不阻塞,并发高,但数据一致性需应用层保证(如乐观锁)。
3、锁定读示例(SELECT ... FOR UPDATE)

3.1. 执行流程

时间 事务 A (隔离级别 RR) 事务 B 状态说明
T1 START TRANSACTION; 事务 A 开启
T2 SELECT stock FROM products WHERE id=1 FOR UPDATE; ‌锁定读‌:读取最新值 10,并加‌排他锁 (X锁)‌
T3 UPDATE products SET stock=5 WHERE id=1; ‌阻塞‌:B 试图获取 X 锁,但被 A 持有,进入等待
T4 UPDATE products SET stock=stock-1 WHERE id=1; ‌成功‌:A 基于当前值 10 扣减,库存变为 9
T5 COMMIT; 事务 A 提交,释放锁
T6 (解除阻塞) UPDATE ... 执行 ‌成功‌:B 获取锁,基于 A 提交后的值 9 继续操作

3.2. 结果分析

‌优势‌:保证了‌串行化效果‌。事务 B 必须等待 A 完成,确保了 A 读取和写入期间数据不被干扰。

‌特点‌:数据强一致,但并发性能低,存在锁等待风险。

4、核心区别总结
维度 非锁定读 (Snapshot Read) 锁定读 (Current Read)
‌SQL 语句‌ SELECT ... SELECT ... FOR UPDATE / FOR SHARE
‌是否加锁‌ ‌否‌,依赖 MVCC 快照 ‌是‌,直接对索引行加锁
‌读取内容‌ 历史版本快照 (可能非最新) 数据库最新已提交数据
‌并发影响‌ 不阻塞其他事务的写操作 阻塞其他事务的写 (或读) 操作
‌典型后果‌ 可能出现不可重复读或丢失更新 确保数据在读写期间的一致性
‌适用建议‌ 大多数查询场景 涉及资金、库存等关键资源的读写序列
5、避坑指南

‌索引必须命中‌:如果 SELECT ... FOR UPDATE 没有走索引(如 WHERE name='xxx' 且无索引),InnoDB 会退化为‌全表锁‌,导致整个表无法被其他事务修改,严重拖垮系统。

‌死锁预防‌:多个事务以不同顺序锁定多行数据时极易死锁。建议业务层固定加锁顺序,或设置合理的超时时间。

‌RR 级别的间隙锁‌:在可重复读级别下,锁定读不仅锁住行,还可能锁定‌间隙‌(即防止其他事务插入新数据),这在处理范围查询时需特别注意,可能意外阻塞正常的插入操作

五、注意事项

‌死锁风险‌:锁定读容易引发死锁。例如,事务 A 锁定了行 1 等待行 2,事务 B 锁定了行 2 等待行 1。务必保持加锁顺序一致,或设置合理的 innodb_lock_wait_timeout。

‌索引的重要性‌:锁定读必须通过‌索引‌来定位行。如果查询没有走索引(全表扫描),InnoDB 可能会锁定整个表的所有行(或大量间隙锁),导致严重的性能下降甚至系统瘫痪。

‌隔离级别影响‌:

在 ‌RC‌ 级别下,FOR UPDATE 只锁住满足条件的行。

在 ‌RR‌ 级别下,FOR UPDATE 除了锁住行,还可能加上‌间隙锁(Gap Lock)‌或‌Next-Key Lock‌,以防止幻读。这意味着即使某些行不存在,也可能被锁定,导致其他事务无法插入这些数据。

‌MySQL 8.0 变化‌:推荐使用 FOR SHARE 替代旧的 LOCK IN SHARE MODE,语义更清晰,且符合 SQL 标准。

相关推荐
流浪0011 小时前
MySQL数据库基础篇(三):MySQL 数据库库级操作全解:创建、字符集、管理与备份恢复实战
数据库·mysql
吴声子夜歌2 小时前
Redis 3.x——集群运维
运维·数据库·redis
钝挫力PROGRAMER2 小时前
MySQL 数据截断错误 #22001:原因分析与解决方案
数据库·mysql
Database_Cool_2 小时前
记忆张量MemOS + 阿里云PolarDB一站式记忆管理方案发布:给AI装上不断片的记忆
数据库·人工智能·阿里云
码农阿豪2 小时前
上线前能跑,上线后挂了,三个隐性的SQL陷阱
数据库·sql
春卷同学2 小时前
032-关系型数据库在记账应用中的建模与查询优化
java·jvm·数据库
foolishlee2 小时前
openGauss慢SQL记录相关线程
数据库
乖巧的妹子3 小时前
SQL知识点
数据库·sql·oracle
数行拙笔3 小时前
Redis---list类型
数据库·redis·缓存