这是一个很好的问题,答案是:MVCC本身的核心机制不依赖于锁来实现读写并发,但在实际的数据库实现中,锁仍然是必要的,用于处理“写-写”冲突和数据一致性。
简单来说,可以把 MVCC 和锁看作是互补的两种机制,分别解决不同的问题。下面我分层次详细解释:
1. MVCC 的无锁核心(解决“读-写”冲突)
MVCC的核心思想是避免让读操作和写操作相互阻塞。它通过以下机制实现,整个过程(读历史版本)完全不需要加锁:
- 多版本: 每次修改数据时,并不直接覆盖原有数据,而是创建一个新的数据版本,并标记版本号(如事务ID、时间戳)。
- 快照读: 每个事务在开始时,会获取一个“一致性视图”或“快照”。在这个事务的生命周期内,所有的读操作都基于这个快照,只读取在该快照之前已提交的数据版本。
- 版本链: 每一行数据可能有多个版本,通过指针连接成一个链表。读操作根据当前事务的快照信息,沿着版本链找到“可见”的那个旧版本。
结论: 正是因为读操作读取的是历史的、已提交的旧版本,而写操作创建新版本,所以读事务和写事务之间不会相互阻塞,也无需加共享锁或排他锁。这是MVCC最大的优势,极大地提升了数据库的并发读性能。
2. 为什么仍然需要锁?(解决“写-写”冲突和一致性)
虽然读写不冲突,但写操作和写操作之间仍然是冲突的。两个事务不能同时修改同一数据的同一版本。这时就必须引入锁机制。
- 对当前数据的锁定: 当一个事务要修改(UPDATE/DELETE)某行数据时,它必须先找到这条数据的“当前最新版本”。在修改这个当前版本之前,数据库必须对其加上锁(通常是行级的排他锁,X锁),以防止其他事务同时修改它,造成更新丢失。
- “当前读”需要锁: 在
READ COMMITTED或REPEATABLE READ隔离级别下,有些特殊的读语句被称为“当前读”,它们读取的是数据的最新版本,而不是快照。这通常也需要加锁。SELECT ... FOR UPDATE/SELECT ... LOCK IN SHARE MODE: 显式加锁。UPDATE/DELETE/INSERT语句: 在执行时,内部会自动对涉及的数据进行“当前读”并加锁。例如,执行UPDATE t SET ... WHERE id=1时,会先对id=1这行数据加X锁,然后修改。
结论: MVCC并不能消除“写-写”冲突。在修改数据时,数据库必须对当前数据版本加锁,以确保数据更新的原子性和一致性。
3. 不同隔离级别下的交互
- 已提交读(Read Committed) 和 可重复读(Repeatable Read):
- 普通SELECT是“快照读”,不加锁(通过MVCC实现)。
- 写操作(UPDATE/DELETE)和锁定读是“当前读”,需要加锁。
- 可串行化(Serializable):
- 为了保证最高级别的隔离,数据库通常会退回到更严格的锁机制。例如,在MySQL InnoDB的Serializable级别下,即使是普通的SELECT也会自动转换为
SELECT ... LOCK IN SHARE MODE,从而加共享锁。此时MVCC的“无锁读”优势基本消失,主要靠锁来保证隔离。
- 为了保证最高级别的隔离,数据库通常会退回到更严格的锁机制。例如,在MySQL InnoDB的Serializable级别下,即使是普通的SELECT也会自动转换为
4. 不同数据库的具体实现
- MySQL InnoDB:
- 完美结合了MVCC和行级锁(记录锁、间隙锁、Next-Key Lock)。
- MVCC解决普通读的并发,行锁解决写并发。间隙锁用于防止幻读。
- PostgreSQL:
- 使用MVCC和多版本存储,其“写-写”冲突的检测不是通过锁,而是通过一种叫做“元组可见性”和“事务状态”的机制。当两个事务尝试更新同一行时,第一个提交的会成功,第二个提交的会失败(报错:
could not serialize access due to concurrent update)。这是通过比较事务ID和行版本号来实现的,而不是阻塞等待锁。但PostgreSQL也有表锁、行锁等机制用于其他场景。
- 使用MVCC和多版本存储,其“写-写”冲突的检测不是通过锁,而是通过一种叫做“元组可见性”和“事务状态”的机制。当两个事务尝试更新同一行时,第一个提交的会成功,第二个提交的会失败(报错:
总结对比
| 操作/场景 | 是否需要锁 | 原因 |
|---|---|---|
| MVCC 快照读(普通SELECT) | 不需要 | 读取历史版本,与当前写操作无冲突 |
| 当前读/写操作(UPDATE/DELETE/锁定读) | 需要 | 必须锁定“当前数据”,解决“写-写”冲突,保证数据一致性 |
| 事务提交 | 需要(隐式) | 提交时需要保证修改的原子性和持久性,涉及日志写入等,系统内部有锁或锁存器 |
核心要点: MVCC是一种“乐观”的并发控制机制,它默认读和写不会冲突,让它们各自工作。而锁是一种“悲观”的机制,它默认冲突会发生,所以先加锁保护。在实际数据库(如InnoDB)中,两者协同工作:MVCC优雅地处理了“读-写”并发,而锁则强有力地保障了“写-写”并发的正确性。