理解 MySQL 并发控制时,LBCC 和 MVCC 是绕不开的两个词。它们解决的都是“多个事务同时读写同一份数据时,怎么保证正确性和性能”的问题,但思路完全不同。
一句话概括:LBCC 靠锁控制并发,MVCC 靠多版本控制并发。
LBCC 是基于锁的并发控制
LBCC 全称可以理解为 Lock-Based Concurrent Control,也就是基于锁的并发控制。
最直观的做法是:
- 读的时候加共享锁。
- 写的时候加排他锁。
- 多个读可以共存。
- 写和读、写和写互相阻塞。
这种方式容易理解,也容易保证数据一致性。但它的问题也明显:读写冲突会让并发性能下降。读多写少还好,一旦读写都频繁,锁等待会越来越明显。
在最高隔离级别 Serializable 下,数据库会更倾向于通过锁让事务串行化执行。这种安全性强,但吞吐会下降。
MVCC 是多版本并发控制
MVCC 是 Multi-Version Concurrent Control,多版本并发控制。它的核心思路是:同一行数据可以存在多个版本,读操作不一定要读最新版本,而是根据事务视图读取一个对当前事务可见的版本。
这样普通查询可以做到“读不加锁,读写不冲突”。写事务正在修改某行数据时,另一个事务仍然可以读取旧版本,不必等待写事务释放锁。
这就是 MVCC 的关键价值:用版本链和可见性判断,减少读写互相阻塞。
快照读和当前读
在 InnoDB 里,读可以分成两类:
- 快照读:读取符合当前事务视图的历史版本,不加锁。
- 当前读:读取最新版本,并加锁保证后续操作安全。
普通 select 通常是快照读:
select * from user where id = 1;
它读取的是当前事务可见的版本,不一定是数据库里绝对最新的版本。
下面这些则属于当前读:
select * from user where id = 1 lock in share mode;
select * from user where id = 1 for update;
update user set name = 'new' where id = 1;
delete from user where id = 1;
insert into user(id, name) values (1, 'new');
当前读要拿最新数据,因为它后面往往要修改或保护这行数据,所以必须加锁。
为什么 update 也是当前读
很多人会把读和写分得太开,认为只有 select for update 才是当前读。实际上 update、delete 在修改前也要先定位最新记录。
例如:
update account set balance = balance - 100 where id = 1;
这条 SQL 不能基于旧快照改余额。它必须读取当前最新版本,并对记录加锁,避免其他事务同时修改同一行。
所以 DML 本质上也会发生当前读。
MVCC 不等于完全不用锁
MVCC 解决的是普通快照读和写之间的冲突,不代表数据库完全不用锁。
以下场景仍然离不开锁:
- 更新同一行。
- 删除同一行。
select ... for update。- 唯一索引冲突检测。
- 间隙锁和 Next-Key Lock。
- DDL 和元数据锁。
因此不要把 MVCC 理解成“没有锁”。更准确的说法是:MVCC 让普通一致性读不必阻塞写,也不被写阻塞。
幻读和可重复读怎么理解
在 InnoDB 的可重复读隔离级别下,普通快照读会基于同一个 Read View,因此同一个事务里多次普通查询可以看到一致的数据集合。
这能解决快照读意义上的幻读:同一个事务多次普通 select,不会突然看到其他事务新插入的数据。
但如果用当前读,例如 select for update,读取的是最新版本,并且会加锁。此时又进入锁控制语义,和普通快照读不是一回事。
很多幻读问题说不清,就是因为把快照读和当前读混在了一起。
一个排查思路
遇到事务并发问题,可以先问三件事:
- 这条 SQL 是快照读还是当前读?
- 当前隔离级别是什么?
- 是否命中了索引,锁住的是行、范围还是更多记录?
如果是普通 select,重点看 Read View 和版本可见性。如果是 update/delete/select for update,重点看锁范围、索引和等待链。
最后记忆
LBCC 靠锁,MVCC 靠版本。快照读读历史可见版本,当前读读最新版本并加锁。
理解这三句话,再看事务隔离、幻读、行锁、间隙锁和死锁,就会少很多混乱。



