MySQL 的隔离性由 MVCC(多版本并发控制) 和锁机制共同保证。
MVCC 主要让普通快照读和写操作尽量互不阻塞,锁则负责处理写与写之间、以及当前读与写之间的冲突。
两套东西在不同隔离级别下组合方式不同,隔离性的强弱就是这么来的。
并发场景下,一个事务在读,另一个事务在写,怎么保证读到的是已提交的完整结果,避免看到中间状态?
这是隔离性要回答的核心问题。

平时写 SQL,你多半从没主动加过锁,这事也不用你管。
InnoDB 在执行 UPDATE、DELETE、INSERT 等写操作时,会自动获取执行这些操作所需的锁,其中常见的是排他锁;
对于 UPDATE、DELETE 以及锁定读,还可能根据查询条件和索引情况涉及记录锁、Gap Lock 或 Next-Key Lock。
你也可以用 SELECT ... FOR UPDATE 手动加排他锁,用 SELECT ... LOCK IN SHARE MODE(MySQL 8.0 之后叫 FOR SHARE)加共享锁。
在很多资料里,排他锁也被称为写锁,共享锁也常被称为读锁。
但要注意,普通 SELECT 在 InnoDB 中通常是 MVCC 快照读,不会因为读取数据就自动加共享锁;
而 UPDATE、DELETE、SELECT ... 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_locks 和 data_lock_waits,哪张表、哪一行、谁等谁,一眼看清。
查询时重点看 OBJECT_SCHEMA、OBJECT_NAME、LOCK_TYPE 和 LOCK_MODE:
LOCK_TYPE 能区分是表级锁还是记录级锁,LOCK_MODE 则能看到 S、X、GAP、REC_NOT_GAP 等锁定信息。
另外,sys.innodb_lock_waits 视图把锁等待关系直接拼好了,排查死锁更省事。
小贴士
锁越多越好吗?
不一定。
从 RC 切换到 RR 后,范围查询和范围更新在 RR 下可能引入 Gap Lock 和 Next-Key Lock,锁定范围比 RC 更大,因此锁等待和死锁风险可能增加。
选隔离级别,是在"看到的数据一致性"和"并发吞吐量"之间做取舍,别一刀切。
平时写业务 SQL 不用操心锁,MySQL 自己会加;
但一旦涉及"先查后改"、"范围更新",就得明白背后加的是行锁还是临键锁,不然并发一上来,死锁和锁等待能让你怀疑人生。