Skip to content

MVCC 与并发控制 ​

什么是 MVCC ​

MVCC(Multi-Version Concurrency Control,多版本并发控制)通过保存数据的多个版本,让读写操作不互相阻塞,实现高并发。

传统锁 vs MVCC ​

传统锁:

  • 读加共享锁,写加排他锁
  • 读写互斥,并发低

MVCC:

  • 读不加锁,写也不阻塞读
  • 每个事务看到数据的一个快照
  • 读写并发度高

MVCC 原理 ​

每行数据有多个版本,每个版本有:

  • 事务 ID:创建这个版本的事务
  • 数据内容:该版本的数据
  • 可见性信息:哪些事务可以看到这个版本

示例:更新操作 ​

sql
-- 初始数据:balance = 100(版本 V1)

-- 事务 1:开始(事务 ID = 100)
BEGIN;
UPDATE accounts SET balance = 200 WHERE id = 1;  -- 创建版本 V2
-- 未提交

-- 事务 2:开始(事务 ID = 101)
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- 仍然读到 V1(100)
COMMIT;

-- 事务 1
COMMIT;  -- V2 生效

-- 事务 3
SELECT balance FROM accounts WHERE id = 1;  -- 读到 V2(200)

工作原理:

  • 事务 1 创建新版本 V2(balance = 200),但未提交
  • 事务 2 开始时创建快照,只能看到 V1
  • 事务 1 提交后,V2 对新事务可见

PostgreSQL 的 MVCC ​

数据结构 ​

每行有隐藏列:

  • xmin:创建该版本的事务 ID
  • xmax:删除该版本的事务 ID(0 表示未删除)
sql
-- 查看隐藏列
SELECT xmin, xmax, * FROM accounts;

输出:

text
 xmin | xmax | id | balance
------+------+----+---------
  100 |    0 |  1 |  200

可见性规则 ​

事务读取数据时,检查:

  1. xmin 已提交且 ≤ 快照事务 ID → 可见
  2. xmax 未提交或 > 快照事务 ID → 可见
  3. 否则不可见

VACUUM ​

旧版本积累会占用空间,VACUUM 清理:

sql
-- 手动 VACUUM
VACUUM accounts;

-- 完整 VACUUM(重建表)
VACUUM FULL accounts;

-- 查看统计
SELECT schemaname, tablename, n_dead_tup, n_live_tup
FROM pg_stat_user_tables;

自动 VACUUM:PostgreSQL 默认启用,定期清理。

MySQL InnoDB 的 MVCC ​

Undo Log ​

InnoDB 使用 Undo Log 存储旧版本:

  • 当前数据在表中
  • 旧版本在 Undo Log 中
  • 通过回滚指针链接版本

Read View ​

每个事务创建 Read View(快照):

  • trx_id_min:最小活跃事务 ID
  • trx_id_max:下一个事务 ID
  • trx_id_list:活跃事务列表

可见性判断 ​

text
IF 行的 trx_id < trx_id_min THEN
    可见(已提交)
ELSE IF 行的 trx_id >= trx_id_max THEN
    不可见(未来事务)
ELSE IF 行的 trx_id IN trx_id_list THEN
    不可见(活跃事务)
ELSE
    可见(已提交)

Purge ​

后台线程定期清理 Undo Log 中的旧版本。

MVCC 与隔离级别 ​

READ COMMITTED ​

每条 SQL 创建新快照:

sql
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- 快照 1:100

-- 其他事务提交:balance = 200

SELECT balance FROM accounts WHERE id = 1;  -- 快照 2:200(新快照)
COMMIT;

REPEATABLE READ ​

事务开始时创建快照,整个事务使用同一快照:

sql
BEGIN;  -- 创建快照
SELECT balance FROM accounts WHERE id = 1;  -- 100

-- 其他事务提交:balance = 200

SELECT balance FROM accounts WHERE id = 1;  -- 仍然 100(同一快照)
COMMIT;

MVCC 的优势 ​

  1. 读写不阻塞:读操作不加锁,写操作不阻塞读
  2. 高并发:多个事务可以并发执行
  3. 一致性快照:事务看到一致的数据视图

MVCC 的代价 ​

  1. 空间开销:存储多个版本
  2. 清理成本:需要定期 VACUUM/Purge
  3. 复杂性:实现和调试更复杂

实际应用 ​

长时间运行的查询 ​

sql
-- 不会阻塞其他事务的写操作
BEGIN;
SELECT COUNT(*) FROM orders WHERE order_date > '2025-01-01';  -- 耗时查询
COMMIT;

-- 其他事务可以正常 INSERT/UPDATE

报表生成 ​

sql
-- 创建一致性快照
BEGIN ISOLATION LEVEL REPEATABLE READ;

-- 生成报表(多个查询)
SELECT SUM(amount) FROM orders;
SELECT COUNT(*) FROM users;
-- 数据一致,不受其他事务影响

COMMIT;

避免幻读 ​

sql
-- MySQL REPEATABLE READ
BEGIN;
SELECT COUNT(*) FROM orders WHERE user_id = 1;  -- 5

-- 其他事务插入
-- INSERT INTO orders (user_id, ...) VALUES (1, ...);

SELECT COUNT(*) FROM orders WHERE user_id = 1;  -- 仍然 5(MVCC 快照)
COMMIT;

监控与优化 ​

PostgreSQL ​

sql
-- 查看表膨胀
SELECT schemaname, tablename, 
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size,
    n_dead_tup
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;

-- 配置自动 VACUUM
ALTER TABLE accounts SET (autovacuum_vacuum_threshold = 1000);

MySQL ​

sql
-- 查看 Undo Log 大小
SHOW ENGINE INNODB STATUS;

-- 查看 Purge 进度
SELECT * FROM information_schema.innodb_trx;

练习 ​

  1. 查看版本:在 PostgreSQL 中查看 xmin、xmax,观察版本变化。
  2. 快照测试:在 REPEATABLE READ 下,验证快照的一致性。
  3. VACUUM:执行 VACUUM,观察空间回收。
  4. 并发测试:多个会话并发读写,验证 MVCC 不阻塞。
  5. 监控:查看表膨胀和死元组数量。
  6. 隔离级别对比:对比 READ COMMITTED 和 REPEATABLE READ 的快照行为。
  7. 长事务问题:启动长事务,观察 VACUUM 受阻。
  8. 实际场景:设计报表系统,使用 MVCC 实现一致性快照。

完成练习后,打开本章答案页核对。

本章小结 ​

第五章学习了事务和并发控制:

  • 事务基础:BEGIN、COMMIT、ROLLBACK
  • ACID 原则:原子性、一致性、隔离性、持久性
  • 隔离级别:READ UNCOMMITTED 到 SERIALIZABLE
  • 锁机制:共享锁、排他锁、死锁处理
  • MVCC:多版本并发控制,实现高并发

第六章将学习索引与性能优化。

回到第五章概览 · 返回SQL 学习路线

用心记录每一步成长