前言
事务这四个字母谁都能背,但真正有用的是搞清楚每个特性由哪个机制兜底,以及隔离级别是怎么"看到不同数据"的。这篇按 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 又拖慢清理