MVCC的实现原理
文章目录
- 前言
- 什么是MVCC?
- 为什么需要MVCC?
- 传统锁机制的问题
- MVCC的优势
- MVCC的实现原理
- 核心组件
- 1. 隐藏字段
- 2. Undo Log(回滚日志)
- 3. Read View(读视图)
- 版本可见性判断
- MVCC在不同隔离级别下的表现
- READ COMMITTED(读已提交)
- REPEATABLE READ(可重复读)
- MVCC的局限性
- 1. 幻读问题
- 2. 写写冲突需要锁
前言
为什么在MySQL中,一个事务在读取数据的时候,另一个事务同时修改同一条数据,读事务却不会被阻塞?这背后的功臣就是MVCC。
什么是MVCC?
MVCC(多版本并发控制),是一种并发控制方法,主要用于数据库管理系统中,同时为事务提供对数据库的并发访问。MVCC让数据库可以为同一行数据维护多个版本,不同的事务可以看到不同版本的数据,从而实现不加锁的情况下解决读写冲突。
为什么需要MVCC?
传统锁机制的问题
在没有MVCC之前,数据库主要依靠锁来解决并发问题:
这种方式存在明显问题:
- 性能低下:读写操作互相阻塞
- 并发度差:大量读操作会严重影响写操作
- 死锁风险:复杂的锁竞争容易产生死锁
MVCC的优势
MVCC带来的好处:
- 读写并发:读操作不会阻塞写操作,写操作也不会阻塞读操作
- 高并发:支持更多的并发事务
- 无死锁:读操作永远不会死锁
- 一致性读:保证事务读取数据的一致性
MVCC的实现原理
核心组件
MVCC主要依靠以下几个组件实现:
1. 隐藏字段
InnoDB为每行记录添加了几个隐藏字段:
- DB_TRX_ID:最后修改该行的事务ID
- DB_ROLL_PTR:指向undo log中该行的上一个版本
- DB_ROW_ID:单调递增的行ID(当表没有主键时使用)
2. Undo Log(回滚日志)
Undo Log记录了数据的历史版本,形成一个版本链。
3. Read View(读视图)
Read View是事务开始时的数据库快照,包含:
- m_ids:当前活跃事务ID列表
- min_trx_id:活跃事务中最小的事务ID
- max_trx_id:下一个要分配的事务ID
- creator_trx_id:创建该Read View的事务ID
版本可见性判断
当事务读取数据时,需要判断某个版本是否对当前事务可见:
// 伪代码:版本可见性判断
public boolean isVisible(long trx_id, ReadView readView) {if (trx_id == readView.creator_trx_id) {// 是当前事务修改的,可见return true;}if (trx_id < readView.min_trx_id) {// 在所有活跃事务之前提交的,可见return true;}if (trx_id >= readView.max_trx_id) {// 在Read View创建之后开启的事务,不可见return false;}if (readView.m_ids.contains(trx_id)) {// 在活跃事务列表中,不可见return false;}// 已提交的事务,可见return true;
}
MVCC在不同隔离级别下的表现
READ COMMITTED(读已提交)
在RC隔离级别下,每次SELECT都会创建新的Read View:
特点:
- 每次读取都创建新的Read View
- 能读到其他事务已提交的修改
- 解决了脏读,但存在不可重复读问题
REPEATABLE READ(可重复读)
在RR隔离级别下,同一事务中的所有SELECT使用同一个Read View:
特点:
- 事务开始时创建Read View,整个事务期间复用
- 保证了可重复读
- MySQL默认隔离级别
MVCC的局限性
1. 幻读问题
MVCC无法完全解决幻读问题:
-- 事务A
BEGIN;
SELECT * FROM users WHERE age > 18; -- 返回5条记录-- 事务B
INSERT INTO users (name, age) VALUES ('Charlie', 25);
COMMIT;-- 事务A
SELECT * FROM users WHERE age > 18; -- 仍然返回5条记录(MVCC保证)-- 但是...
UPDATE users SET name = CONCAT(name, '_updated') WHERE age > 18;
-- 会更新6条记录!包括事务B插入的记录
2. 写写冲突需要锁
MVCC只解决了读写冲突,写写冲突仍需要锁:
-- 两个事务同时更新同一行
-- 事务A
BEGIN;
UPDATE users SET name = 'Alice' WHERE id = 1; -- 获得行锁-- 事务B
BEGIN;
UPDATE users SET name = 'Bob' WHERE id = 1; -- 被阻塞,等待行锁