你以为隔离级别只是个开关,可 MySQL 凭什么靠它挡住幻读?

MySQL 的隔离性由 MVCC(多版本并发控制)锁机制共同保证。

MVCC 主要让普通快照读和写操作尽量互不阻塞,锁则负责处理写与写之间、以及当前读与写之间的冲突。

两套东西在不同隔离级别下组合方式不同,隔离性的强弱就是这么来的。

并发场景下,一个事务在读,另一个事务在写,怎么保证读到的是已提交的完整结果,避免看到中间状态?

这是隔离性要回答的核心问题。

平时写 SQL,你多半从没主动加过锁,这事也不用你管。

InnoDB 在执行 UPDATEDELETEINSERT 等写操作时,会自动获取执行这些操作所需的锁,其中常见的是排他锁;

对于 UPDATEDELETE 以及锁定读,还可能根据查询条件和索引情况涉及记录锁、Gap Lock 或 Next-Key Lock。

你也可以用 SELECT ... FOR UPDATE 手动加排他锁,用 SELECT ... LOCK IN SHARE MODE(MySQL 8.0 之后叫 FOR SHARE)加共享锁。

在很多资料里,排他锁也被称为写锁,共享锁也常被称为读锁。

但要注意,普通 SELECT 在 InnoDB 中通常是 MVCC 快照读,不会因为读取数据就自动加共享锁;

UPDATEDELETESELECT ... FOR UPDATE 等操作则会根据语句和隔离级别获取相应的锁。

也就是说,不是所有读都会加锁,InnoDB 会根据读的类型决定是走 MVCC,还是走锁定读。

锁的粒度:表锁和行锁

先把锁按"管多大范围"分一下。

表锁的锁定范围是整张表,并发粒度比行锁粗,多个事务对同一张表的访问更容易互相阻塞。

它简单、开销小,但并发能力远低于行锁。

InnoDB 的数据并发控制主要依赖行级锁,同时也存在表级锁、意向锁等机制;

而 MyISAM 的并发控制主要依赖表锁,这也是 InnoDB 在高并发事务场景下更有优势的原因之一。

行级锁的锁定粒度比表锁细,通常围绕具体记录或索引范围进行控制。

其他不冲突的记录通常仍然可以正常读写,因此在高并发场景下拥有更好的并发能力。

打个比方:

表锁像把整个仓库大门一锁,所有货架都进不去;

行锁像只给某个储物柜挂了把锁,其他人还能开自己的柜子。

高并发下,后者明显更省时间,要的正是这个"只动该动的"精度。

行锁的两种模式:共享锁和排他锁

行级锁还可以从"是否允许多个事务同时持有"这个角度,分成共享锁和排他锁两种模式。

**共享锁(S 锁)**允许多个事务同时持有同一记录的共享锁,但持有共享锁的事务不能修改这条记录。

需要注意的是,共享锁对应的是锁定读,而不是普通 SELECT

例如 SELECT ... FOR SHARE 会对读取到的记录加共享锁,而普通 SELECT 通常走 MVCC 快照读,并不会因为读取数据就加 S 锁。

排他锁(X 锁),同一行同一时刻只能被一个事务加。

加了排他锁的行,别的事务既加不了共享锁,也加不了排他锁,直到它释放。

"排他"就是不让别人再锁。

打个比方:

共享锁像一个公共储物柜的"只读预约",多人可以同时查看,但谁都不能在持有共享锁期间修改;

排他锁则像给柜子上了私锁,其他事务不能再获取与它冲突的锁。

sql 复制代码
-- 手动加共享锁(读共享,期间别人能读不能改)
SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE;

-- 手动加排他锁:阻止其他事务再对该记录加冲突锁,
-- 但普通 SELECT 通常仍可通过 MVCC 读取历史版本
SELECT * FROM orders WHERE id = 1 FOR UPDATE;

实际业务里,SELECT ... FOR UPDATE 经常出现在"先查出来、再判断、再更新"的流程里。

它帮你在业务层把普通读变成当前读,读的那一刻就把行锁死,避免别人中间插队修改。

一个经典的死锁模式是:

两个事务都先拿到同一记录的共享锁,随后又都尝试获取排他锁,就可能互相等待对方释放共享锁,形成循环等待。

实际写代码时,能直接加排他锁就别先共享再升级。

间隙锁和临键锁:专门收拾幻读

共享锁、排他锁只能管住"已有这一行"的冲突。

别的事务要是在中间插了新行,第二次查就会多出几行------这就是幻读。

光靠行锁锁不住"还不存在的行",所以 InnoDB 在行锁基础上又加了两种锁。

间隙锁(Gap Lock),不锁具体的数据行,而是锁住两条记录之间的"缝"。

订单表 id 有 1、3、5 三条记录,(1,3)、(3,5)、(5,+∞) 就是缝。

间隙锁把这个范围锁住,其他事务就不能在被锁定的间隙中插入新记录,从而防止当前读再次执行时出现新的匹配行。

临键锁(Next-Key Lock),是 InnoDB 在 RR 隔离级别下用于范围锁定的重要锁形式,本质上等于"记录锁 + 前面的间隙锁"。

它既锁住某条记录,又锁住它前面那条缝,组合成一个左开右闭 的区间。还是上面的例子,概念上可以理解为 (-∞,1](1,3](3,5] 以及 (5,+∞)

具体加锁范围还会受到查询条件、索引类型以及是否唯一索引等因素影响。

记录锁锁的是已有的行,间隙锁锁的是行与行之间的缝。

临键锁把两者组合起来,既保护已有记录,又阻止其他事务向锁定范围内插入新记录。

代价是锁定范围可能比单条记录更大,因此在 RR 下执行大范围更新或锁定读时,更容易扩大锁冲突范围,产生锁等待。

在 InnoDB 中,Gap Lock 和 Next-Key Lock 主要出现在 RR 隔离级别的锁定读、UPDATE、DELETE 等当前读场景里,具体锁定范围高度依赖 SQL 能否利用索引。

如果没有合适的索引可用于这条语句,InnoDB 可能需要扫描大量记录并对涉及的索引记录加锁------底层不一定真的升级成传统意义上的表锁,但锁定范围可能非常大,效果上接近"整张表都被锁住",并发性能会明显下降。

四个隔离级别,本质是两套机制的不同组合

隔离性到底有多强,看每个级别怎么摆 MVCC 和锁。

隔离级别 MVCC 锁怎么用 能挡住什么
读未提交(RU) 不提供一致性快照隔离,普通读可以看到其他事务尚未提交的数据 锁定语义较弱 可能出现脏读、不可重复读、幻读
读已提交(RC) 用(每次一致性读生成新 ReadView) 写操作和当前读主要使用记录锁,通常不使用 Gap Lock,因此范围内仍允许插入新记录 防止脏读,但可能出现不可重复读和幻读
可重复读(RR) 用(首次快照读生成 ReadView,之后沿用) 当前读配合记录锁、Gap Lock、Next-Key Lock 防止脏读和不可重复读,并在锁定读场景中抑制幻读(InnoDB 默认)
串行化(Serializable) 普通 SELECT 也会采用锁定读语义 读通常加共享锁,写加排他锁,并通过更强的锁冲突限制并发 隔离性最强,并发能力最低

读未提交最粗暴,普通读取直接能看到其他事务尚未提交的最新版本,所以会读到别的事务还没提交的脏数据。

这个级别线上几乎不用,只有极端调试场景才会看到。

读已提交靠 MVCC 快照读解决了脏读,但写操作主要使用记录锁,不像 RR 那样普遍用 Gap Lock 和 Next-Key Lock 来保护范围,所以别的事务仍然可以在相应范围内插入新行或修改数据,因此不可重复读和幻读仍然可能发生。

Oracle 默认就是 RC,所以很多从 Oracle 转 MySQL 的团队会顺手把 MySQL 也改成 RC,但得接受这个代价。

可重复读是 InnoDB 的默认级别,也是日常最常用的。

它第一次快照读时生成一个 ReadView,之后整个事务都沿用这一个快照,所以多次读同一行结果一致;

碰到"当前读"(比如 FOR UPDATE、UPDATE、DELETE)时,InnoDB 会根据查询条件和索引情况使用记录锁、Gap Lock 或 Next-Key Lock 来保护相应的记录和范围,从而抑制幻读。

MVCC 解决"读的稳定性",临键锁解决"写的边界",两者配合才是完整隔离。

串行化最极端。

在 InnoDB 中,事务处于 SERIALIZABLE 隔离级别时,普通 SELECT 在非自动提交事务中也会采用锁定读语义,通常相当于隐式加上 FOR SHARE,从而让读写之间更容易发生锁等待,把并发执行进一步限制在接近串行的状态。

隔离性最高,但并发能力也最低,只适合并发量较低、事务之间必须严格避免并发异常的特殊场景。

就我的经验看,实际项目里基本在 RC 和 RR 之间二选一,串行化很少碰。

这里有一个容易搞混的点:

普通快照读通常走 MVCC,不获取记录锁;

当前读则需要读取最新版本并获取相应的锁(FOR UPDATE、UPDATE、DELETE 都属于当前读)。

同样是 RR,你普通的 SELECT 看的是 ReadView,而 SELECT FOR UPDATE 要抢锁。

面试里问"RR 到底怎么解决幻读",一定要区分快照读和当前读:

快照读通过固定 ReadView 保证同一事务中多次读取看到一致的数据版本;

当前读则通过记录锁、Gap Lock 和 Next-Key Lock 等机制保护查询范围,避免并发插入影响当前读结果。

想看锁到底加了没,怎么查

真遇到锁等待、死锁,别靠猜。

InnoDB 给了现成的观察口:

SHOW ENGINE INNODB STATUS 可以查看 InnoDB 的运行状态,其中 TRANSACTIONS 部分能够帮助分析事务、锁等待以及最近检测到的死锁等信息。

MySQL 8.0 之后更推荐直接查 performance_schema.data_locksdata_lock_waits,哪张表、哪一行、谁等谁,一眼看清。

查询时重点看 OBJECT_SCHEMAOBJECT_NAMELOCK_TYPELOCK_MODE

LOCK_TYPE 能区分是表级锁还是记录级锁,LOCK_MODE 则能看到 SXGAPREC_NOT_GAP 等锁定信息。

另外,sys.innodb_lock_waits 视图把锁等待关系直接拼好了,排查死锁更省事。

小贴士

锁越多越好吗?

不一定。

从 RC 切换到 RR 后,范围查询和范围更新在 RR 下可能引入 Gap Lock 和 Next-Key Lock,锁定范围比 RC 更大,因此锁等待和死锁风险可能增加。

选隔离级别,是在"看到的数据一致性"和"并发吞吐量"之间做取舍,别一刀切。

平时写业务 SQL 不用操心锁,MySQL 自己会加;

但一旦涉及"先查后改"、"范围更新",就得明白背后加的是行锁还是临键锁,不然并发一上来,死锁和锁等待能让你怀疑人生。

相关推荐
创新技术阁15 分钟前
FastapiAdmin 前后端启动全流程详解
前端·后端·fastapi
七牛开发者20 分钟前
实测推荐 3 个 Skill,轻松上手 Codex 网页设计与交付流程
前端·javascript·后端
七牛开发者23 分钟前
拆解 dsh 系列:从源码和版本变化看 DeepSeek Harness 的设计取舍
前端·javascript·后端
luteres36 分钟前
Spring学习笔记
java·后端·spring
深念Y36 分钟前
登录日志与管理员审计日志存储决策
前端·arm开发·后端·微服务·云原生·架构
用户8356290780511 小时前
使用 Python 为 PowerPoint 演示文稿添加评论
后端·python
jvmind_dev1 小时前
SWT 堆外内存泄漏排查实录:一次 PNG 保存泄漏一张图,一行 g_free 治好
java·后端
南雨北斗1 小时前
TP6 安全规范的接收post请求与安全机制分析
后端
南雨北斗1 小时前
TP6 防范XSS攻击 响应头防御(设置内容安全策略 :CSP)
后端