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:创建该版本的事务 IDxmax:删除该版本的事务 ID(0 表示未删除)
sql
-- 查看隐藏列
SELECT xmin, xmax, * FROM accounts;输出:
text
xmin | xmax | id | balance
------+------+----+---------
100 | 0 | 1 | 200可见性规则
事务读取数据时,检查:
xmin已提交且 ≤ 快照事务 ID → 可见xmax未提交或 > 快照事务 ID → 可见- 否则不可见
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:最小活跃事务 IDtrx_id_max:下一个事务 IDtrx_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 的优势
- 读写不阻塞:读操作不加锁,写操作不阻塞读
- 高并发:多个事务可以并发执行
- 一致性快照:事务看到一致的数据视图
MVCC 的代价
- 空间开销:存储多个版本
- 清理成本:需要定期 VACUUM/Purge
- 复杂性:实现和调试更复杂
实际应用
长时间运行的查询
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;练习
- 查看版本:在 PostgreSQL 中查看 xmin、xmax,观察版本变化。
- 快照测试:在 REPEATABLE READ 下,验证快照的一致性。
- VACUUM:执行 VACUUM,观察空间回收。
- 并发测试:多个会话并发读写,验证 MVCC 不阻塞。
- 监控:查看表膨胀和死元组数量。
- 隔离级别对比:对比 READ COMMITTED 和 REPEATABLE READ 的快照行为。
- 长事务问题:启动长事务,观察 VACUUM 受阻。
- 实际场景:设计报表系统,使用 MVCC 实现一致性快照。
完成练习后,打开本章答案页核对。
本章小结
第五章学习了事务和并发控制:
- 事务基础:BEGIN、COMMIT、ROLLBACK
- ACID 原则:原子性、一致性、隔离性、持久性
- 隔离级别:READ UNCOMMITTED 到 SERIALIZABLE
- 锁机制:共享锁、排他锁、死锁处理
- MVCC:多版本并发控制,实现高并发
第六章将学习索引与性能优化。