MySQL事务原理解析
一、事务与 ACID 要点(概念)
事务(Transaction):一组作为单一逻辑工作单元的 SQL 操作,要么全部成功(commit),要么全部失败回滚(rollback)。
ACID:
- Atomicity(原子性):操作要么全部完成,要么全部不做。
- Consistency(一致性):事务执行前后,数据库要保持一致性约束(外键、触发器、检查约束等)。
- Isolation(隔离性):并发事务间相互隔离,互不干扰(由隔离级别决定)。
- Durability(持久性):事务提交后对数据库的修改应当持久保存,即使崩溃也能恢复(由 redo log 等保证)。
二、基本事务命令与行为
SET autocommit = 1|0;:autocommit = 1(默认)表示每条语句自动作为一个事务提交;autocommit=0则需要手动COMMIT/ROLLBACK。开启事务:
START TRANSACTION;或BEGIN;- 也可
BEGIN WORK;
提交:
COMMIT;
回滚:
ROLLBACK;
控制隔离级别(会话或全局):
- 会话级:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; - 全局:
SET GLOBAL ...(需重连才能生效会话)
- 会话级:
显式锁:
- 行级写锁:
SELECT ... FOR UPDATE; - 共享读锁(旧语法):
SELECT ... LOCK IN SHARE MODE;(MySQL 8.0 有FOR SHARE)
- 行级写锁:
三、并发现象(通过隔离级别来理解)
常见三类并发问题:
- Dirty Read(脏读):读取到未提交事务写入的数据(若该事务回滚,读取到的是不存在的数据)。
- Non-repeatable Read(不可重复读):同一事务内两次读取同一行,第二次读到的值与第一次不同(因为其它事务提交了更新)。
- Phantom Read(幻读):同一事务中两次执行相同条件的范围查询,第二次多出(或少了)几行(因为其它事务插入或删除了满足条件的行)。
四种 SQL 标准隔离级别(从弱到强):
READ UNCOMMITTED
- 允许脏读;基本不使用于 InnoDB(InnoDB 默认不会出现脏读)。
READ COMMITTED
- 防止脏读(只能读已提交的数据)。
- 不可重复读可能发生。
- 每次
SELECT使用最新提交的版本(即读时不会建立固定快照)。
REPEATABLE READ(MySQL/InnoDB 默认)
- 防止脏读与不可重复读。
- 在 InnoDB 中可通过 MVCC(多版本并发控制)+ gap/next-key locks 同时避免幻读(在很多情况下也能避免幻读)。
- 注意:标准 SQL 的 REPEATABLE READ 未必防止幻读,但 InnoDB 的实现能避免幻读(详见后文)。
SERIALIZABLE
- 最强隔离,通过对读取操作加锁(将读取当成加锁操作)达到串行化执行效果,从而防止所有三种现象,但并发性能最低。
四、InnoDB 的事务实现核心(重点)
InnoDB 是 MySQL 的主流事务型引擎,其实现关键点:MVCC(多版本并发控制)、undo log、redo log(重做日志)、锁(行锁 + 间隙锁/next-key)、锁等待与死锁检测、崩溃恢复机制。
1) MVCC(Multi-Version Concurrency Control,多版本并发控制)
- 目的:读操作不阻塞写,写操作尽量不阻塞读,提高并发性。
- 做法:每次事务修改数据时,InnoDB 会写入undo log(旧版本),同时创建当前行的新版本。并发读取根据事务的 read view(快照)读取相应版本,从而实现一致性读(consistent read)。
- Read view(读视图):当事务开始(或执行首个一致性读时)创建,记录当前活跃事务的事务 id 列表。后续读取将基于这个视图决定行是否可见(即是否是提交且事务 id 小于读视图或不在活跃事务列表中)。
- MVCC 可实现 快照读(consistent read),例如
SELECT * FROM t WHERE id=1;(普通 SELECT,在非锁定读情况下)使用快照而不是加锁,从而不会阻塞写。但SELECT ... FOR UPDATE是锁定读,会加锁。
2) undo log 与 redo log(日志)
Undo log(回滚日志):
- 用于保存数据的旧版本(用于事务回滚以及提供 MVCC 的旧值给其他并发事务读取)。
- Undo 存放在 undo 表空间(InnoDB 可配置 undo tablespaces)。
- 写入 Undo 并不代表持久化到磁盘(undo 主要用于回滚和一致性读)。
Redo log(重做日志):
- 在事务提交之前,InnoDB 会把修改写到 redo log(按 WAL 原则,先写日志后改数据),保证崩溃恢复(持久性)。
- redo log 在刷盘(fsync)到磁盘后事务才被认为持久。
- InnoDB 使用 doublewrite buffer、log buffer、log file 来保证写入的安全性与速度。
Checkpoint、Flush、Purge:
- 数据页在缓冲池修改后是脏页,需要定期刷回磁盘(checkpoint)。
- Undo 会随着事务结束积累,需要 purge 线程清理不再被任何 read view 需要的 undo 记录,防止 undo 表空间无限增长。
3) 锁机制:行锁、间隙锁(gap lock)、next-key lock、意向锁
行级锁(record lock):锁定具体索引记录(主键或索引上的行)。
间隙锁(gap lock):锁定索引中的区间(不锁记录本身,只锁区间),用于避免幻读(阻止其他事务在该区间插入新行)。
next-key lock = record lock + gap lock(对索引记录以及它左侧的 gap 都加锁),InnoDB 在 REPEATABLE READ 默认使用 next-key locks 来防止幻读(范围扫描时)。
意向锁(Intention locks):表级标记,表示事务将在行上加何种锁(用于多粒度锁兼容检查),例如
IX(意向排他锁)或IS(意向共享锁)。何时加锁?
- 普通
SELECT(非锁定读)在 InnoDB 中通常使用 MVCC,不加锁。 SELECT ... FOR UPDATE、UPDATE、DELETE会对匹配的行加排他锁(record lock),并可能对范围加 gap/next-key lock。- 范围查询在某些隔离级别会产生 gap locks,从而阻止其他事务插入满足范围条件的新记录(用于防止幻读)。
- 普通
4) 死锁检测与锁等待
- 当两个或多个事务互相等待对方释放锁时会形成死锁。InnoDB 采用检测并回滚死锁中的某个事务(而不是采用超时回滚),可以通过查看
SHOW ENGINE INNODB STATUS;来定位死锁原因与回滚的事务。 - 也有可能出现锁等待超时(不是死锁),默认超时由
innodb_lock_wait_timeout控制(秒),超过则报错并回滚当前语句。
5) 崩溃恢复
- 基于 redo log(重做日志)和 undo,InnoDB 在崩溃恢复时重做已提交的事务(redo),回滚未提交的事务(undo),从而恢复到一致状态。
五、四个隔离级别的 MySQL / InnoDB 具体实现差异(重点)
READ UNCOMMITTED:
- 允许读取未提交的数据(脏读)。InnoDB 实际上很少使用此级别。
READ COMMITTED:
- 每次读取都基于最新的提交(每次语句都会创建新的 read view),因此避免脏读;不可重复读/幻读仍可能发生。
- 实际表现:
SELECT->读到的是当前已提交的数据(非 snapshot repeatable)。
REPEATABLE READ(默认):
- 一致性读使用事务开始时的快照(read view),保证同一事务内的多次读取可重复(不可重复读被避免)。
- 为防止幻读,InnoDB 在范围查询会使用 next-key locks(范围锁),从而阻止其他事务在范围内插入记录,避免幻读。
- 注意:某些情况下 InnoDB 的 MVCC 本身以及 gap locks 工作逻辑很复杂,理解要点是“InnoDB 在默认级别尽量保证可重复读并避免幻读”。
SERIALIZABLE:
- 将普通 SELECT 升级为加共享锁读取(等同
SELECT ... LOCK IN SHARE MODE),从而强制串行化执行。性能代价高。
- 将普通 SELECT 升级为加共享锁读取(等同
六、举例说明(SQL 示例与并发行为)
假设表 t(a INT PRIMARY KEY, v INT)。
脏读示例(READ UNCOMMITTED) 事务 A:
START TRANSACTION; UPDATE t SET v=100 WHERE a=1; -- 不提交事务 B(在 READ UNCOMMITTED 下):
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT v FROM t WHERE a=1; -- 可能读到 100(脏数据)不可重复读示例(READ COMMITTED) 事务 A:
START TRANSACTION; SELECT v FROM t WHERE a=1; -- 读到 v=10事务 B:
START TRANSACTION; UPDATE t SET v=20 WHERE a=1; COMMIT;事务 A(仍在进行):
SELECT v FROM t WHERE a=1; -- 在 READ COMMITTED 下 会读到 20(不可重复读)幻读示例(区别 REPEATABLE READ 与 READ COMMITTED) 事务 A(READ COMMITTED):
START TRANSACTION; SELECT COUNT(*) FROM t WHERE v > 10; -- 返回 5事务 B:
START TRANSACTION; INSERT INTO t(a,v) VALUES (999, 20); COMMIT;事务 A:
SELECT COUNT(*) FROM t WHERE v > 10; -- READ COMMITTED 可能返回 6(幻读)在 REPEATABLE READ 下,InnoDB 利用 MVCC + range locks 来避免幻读(前提是用的是锁定读或涉及范围查询的更新语句会加 gap locks),所以事务 A 在 REPEATABLE READ 会看到一致的快照(通常仍是 5)。实际细节有时依赖于是否是一致性读还是锁定读。
SELECT ... FOR UPDATE与锁行为SELECT * FROM t WHERE id=1 FOR UPDATE;会对 id=1 记录加排他锁,阻止其他事务更新/删除该行,其他事务的普通SELECT(非锁定读)仍可通过 MVCC 读到上一个快照版本(不被阻塞),但UPDATE将被阻塞直到锁释放。
七、事务性能、调优与常见陷阱
选择合适的隔离级别:
- 如果业务能容忍更弱的一致性(例如只需防止脏读),使用
READ COMMITTED可以减少锁冲突并提升并发性。 - 对写密集型、高并发场景,
REPEATABLE READ可能产生更多 gap locks,需权衡。
- 如果业务能容忍更弱的一致性(例如只需防止脏读),使用
避免长事务:
- 长事务会阻止 purge 清理 undo,造成 undo 表空间增长、缓冲池压力,影响性能与磁盘空间。
合理使用索引:
- InnoDB 的行锁锁在索引上,缺少合适索引会导致全表扫描并触发表级锁(或大量锁),并发性能差。
尽量短小、少交互的事务:
- 减少事务中等待输入或长时间计算的步骤,避免持有锁时间过长。
处理死锁:
- 捕获并重试被死锁回滚的事务(应用层逻辑),使用
SHOW ENGINE INNODB STATUS;查找死锁原因。
- 捕获并重试被死锁回滚的事务(应用层逻辑),使用
监控 undo/redo 与 buffer pool:
- 观察
innodb_undo_tablespaces,innodb_buffer_pool_size, redo log 大小(innodb_log_file_size),双写(doublewrite)策略等。
- 观察
binlog 格式与复制的事务一致性:
- MySQL 的 binlog 有三种格式:STATEMENT、ROW、MIXED。以事务性与复制正确性考虑,ROW 模式最安全(但二进制日志更大)。隔离级别对复制有影响(例如 statement-based 在某些并发场景会有问题)。
八、诊断、查看信息的常用命令
SHOW ENGINE INNODB STATUS\G;— 查看最近死锁信息、锁信息、InnoDB 状态(非常重要)。SHOW PROCESSLIST;— 查看等待的线程。SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;/INNODB_LOCK_WAITS(视 MySQL 版本而定) — 查看锁等待细节。SHOW VARIABLES LIKE 'innodb%';— 查看 InnoDB 相关参数(如innodb_lock_wait_timeout、innodb_flush_log_at_trx_commit、innodb_log_file_size等)。EXPLAIN/EXPLAIN ANALYZE— 确认查询是否走索引(以减少锁范围)。
九、补充:悲观锁 vs 乐观锁
- 悲观锁:依赖数据库锁(如
SELECT ... FOR UPDATE)来保证并发安全,典型于需要严密一致性的场景。 - 乐观锁:通过版本号(version)或时间戳字段在应用层实现(读-改-写时检测版本是否变化),适合读多写少、高并发写冲突概率低的场景。
示例乐观锁实现(伪 SQL):
-- 表 t 有 version 字段
UPDATE t SET v=..., version = version + 1 WHERE id=1 AND version = 5;
-- 如果返回的 affected_rows=0,说明冲突,需重试或提示失败十、实践示例(如何在应用层安全使用事务)
- 简单转账示例(伪代码):
START TRANSACTION;
-- 锁定两条记录,避免并发修改
SELECT balance FROM account WHERE id=1 FOR UPDATE;
SELECT balance FROM account WHERE id=2 FOR UPDATE;
UPDATE account SET balance = balance - 100 WHERE id=1;
UPDATE account SET balance = balance + 100 WHERE id=2;
COMMIT;注意:先锁定,再做计算,避免在事务运行中与外部交互或等待用户输入,避免长事务与死锁。
十一、常见问题 FAQ(简短)
- Q:为什么我的
SELECT会被阻塞? A:可能是SELECT ... FOR UPDATE、UPDATE、DELETE等语句被其它事务加了锁。普通SELECT(非锁定读)通常不会被阻塞(使用 MVCC)。 - Q:如何查看死锁原因? A:
SHOW ENGINE INNODB STATUS\G会显示死锁回溯信息。 - Q:InnoDB 默认隔离级别是什么? A:
REPEATABLE READ(MySQL 默认)。 - Q:为什么长事务会影响性能? A:它阻止 purge 清理 undo,使 undo 增大,consistency read 复杂度变高,且持有锁时间长,阻塞其他事务。
十二、结论与建议(要点浓缩)
- 事务是保证数据一致性的核心,选择合适隔离级别是性能与一致性之间的权衡点。
- 大多数 OLTP 应用使用 InnoDB 的默认
REPEATABLE READ即可;但在高并发写场景下考虑降为READ COMMITTED以减少锁争用(需评估业务一致性需求)。 - 避免长事务、使用合适索引、监控
SHOW ENGINE INNODB STATUS,并在应用层对死锁进行重试,是保持事务健壮性的关键。
