MySQL 教程:事务 ACID、四种隔离级别与 MVCC

· Mysql笔记 MySQL InnoDB 事务 · 3 阅读 · 约 5 分钟

前言

事务这四个字母谁都能背,但真正有用的是搞清楚每个特性由哪个机制兜底,以及隔离级别是怎么"看到不同数据"的。这篇按 InnoDB 的实现来讲。

ACID 靠什么实现

  • 原子性:靠 undo log。改动前先写反向日志,回滚时按版本链撤销
  • 一致性:是目标不是手段,由原子性、隔离性、持久性加上约束(主键、外键、非空)和业务代码共同保证
  • 隔离性:靠锁 + MVCC。写写冲突用行锁串行化,读不阻塞写
  • 持久性:靠 redo log。事务提交时把重做日志刷盘,实例崩溃重启后前滚恢复;binlog 负责服务器层的归档与复制

顺带一句"双一"配置:innodb_flush_log_at_trx_commit = 1 加 sync_binlog = 1,是崩溃后不丢事务的默认组合。

基本语法

SELECT @@autocommit;                       -- 默认 ON,每条语句自成一个事务
SET autocommit = 0;                         -- 或者显式 START TRANSACTION
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
SAVEPOINT sp1;
ROLLBACK TO SAVEPOINT sp1;
COMMIT;

要注意 DDL(ALTER TABLE、CREATE TABLE)会隐式提交当前事务,事务里混进建表语句基本达不到"一起回滚"的预期。

四种隔离级别

隔离级别 脏读 不可重复读 幻读 说明
READ UNCOMMITTED 可能 可能 可能 能读到别的事务未提交的数据,几乎不用
READ COMMITTED 不会 可能 可能 Oracle、SQL Server 的默认
REPEATABLE READ 不会 不会 见下 InnoDB 默认
SERIALIZABLE 不会 不会 不会 普通读也加锁,并发度最差

查看和设置:

SELECT @@transaction_isolation;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

5.7 里 transaction_isolation 和旧的 tx_isolation 都能查,新写的脚本建议用前者。会话级设置只影响当前连接,全局改完要重连才对新会话生效。

MVCC 是怎么做到的

InnoDB 每行记录都带两个隐藏列:事务 ID(最近修改这行的事务)和回滚指针(指向 undo log 里的上一个版本)。同一行的历次修改被串成一条版本链。

事务做一致性读时会生成一个 ReadView,里面记着生成时刻还没提交的那批事务 ID,以及高低水位。沿版本链从新往旧走,第一个"事务 ID 对当前 ReadView 可见"的版本就是结果,链头不可见就退到上一个版本。

关键差异在这里:

  • READ COMMITTED:每条语句都重新生成 ReadView,所以别的事务一提交,下一条 SELECT 就能看见——不可重复读
  • REPEATABLE READ:事务里第一条一致性读生成 ReadView 并复用到底,整个事务看到的都是同一个快照

快照读与当前读

SELECT 默认走快照读,不加锁、读历史版本。下面这些是当前读,读的是最新版本并且会加锁:

SELECT ... FOR UPDATE;                 -- 排他锁
SELECT ... LOCK IN SHARE MODE;         -- 共享锁,5.7 的写法
UPDATE ... ;  DELETE ... ;             -- 写操作本身
INSERT ... ON DUPLICATE KEY UPDATE;    -- 同理

幻读问题就是这么处理的:RR 级别下普通 SELECT 靠快照不会看到新插入的行;如果走当前读,InnoDB 用 next-key lock(记录锁加间隙锁)把索引区间一起锁住,别的会话插不进来。所以"RR 完全解决幻读"这个说法要加限定:只在它自己的写和当前读范围内成立。

小结

  • 原子性看 undo,持久性看 redo,隔离性看锁加 MVCC,一致性是结果
  • RR 与 RC 的实现差别只是 ReadView 生成的时机不同
  • 快照读便宜但可能读不到最新值,需要最新值就用 FOR UPDATE 走当前读
  • 事务要短,长事务既撑大 undo 又拖慢清理