mysql 锁定读和非锁定读

📅 2026/7/18 1:22:04
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. 非锁定读默认--事务ASTARTTRANSACTION;SELECTbalanceFROMaccountsWHEREid1;--此时事务B可以修改 id1的 balance事务A不受影响且看到的是快照数据COMMIT;2. 锁定读排他锁--事务ASTARTTRANSACTION;SELECTbalanceFROMaccountsWHEREid1FORUPDATE;--此时事务B如果想执行UPDATEaccountsSETbalance...WHEREid1;--或者SELECT...FORUPDATE事务B会被阻塞直到事务A提交或回滚UPDATEaccountsSETbalancebalance-100WHEREid1;COMMIT;3举例说明1、场景设定假设有一张商品表 products其中 id1 的商品库存 stock 为 ‌10‌。‌事务 A‌查询库存并准备扣减。‌事务 B‌试图修改同一商品的库存。2、非锁定读示例普通 SELECT2.1执行流程时间事务 A (隔离级别 RR)事务 B状态说明T1START TRANSACTION;事务 A 开启T2SELECT stock FROM products WHERE id1;‌非锁定读‌读取快照不加锁T3UPDATE products SET stock5 WHERE id1;‌成功‌因为 A 没加锁B 可以直接修改并提交T4COMMIT;事务 B 提交库存变为 5T5SELECT stock FROM products WHERE id1;‌仍返回 10‌RR 级别下A 看到的是 T1 时刻的快照T6UPDATE products SET stockstock-1 WHERE id1;‌成功‌A 基于快照值 10 计算最终库存变为 92.2. 结果分析‌问题‌发生了‌丢失更新‌。事务 B 将库存改为 5但事务 A unaware基于旧值 10 扣减后变成 9导致 B 的修改被覆盖。‌特点‌读写互不阻塞并发高但数据一致性需应用层保证如乐观锁。3、锁定读示例SELECT … FOR UPDATE3.1. 执行流程时间事务 A (隔离级别 RR)事务 B状态说明T1START TRANSACTION;事务 A 开启T2SELECT stock FROM products WHERE id1 FOR UPDATE;‌锁定读‌读取最新值 10并加‌排他锁 (X锁)‌T3UPDATE products SET stock5 WHERE id1;‌阻塞‌B 试图获取 X 锁但被 A 持有进入等待T4UPDATE products SET stockstock-1 WHERE id1;‌成功‌A 基于当前值 10 扣减库存变为 9T5COMMIT;事务 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 标准。