Skip to content

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 标准隔离级别(从弱到强):

  1. READ UNCOMMITTED

    • 允许脏读;基本不使用于 InnoDB(InnoDB 默认不会出现脏读)。
  2. READ COMMITTED

    • 防止脏读(只能读已提交的数据)。
    • 不可重复读可能发生。
    • 每次 SELECT 使用最新提交的版本(即读时不会建立固定快照)。
  3. REPEATABLE READ(MySQL/InnoDB 默认)

    • 防止脏读与不可重复读。
    • 在 InnoDB 中可通过 MVCC(多版本并发控制)+ gap/next-key locks 同时避免幻读(在很多情况下也能避免幻读)。
    • 注意:标准 SQL 的 REPEATABLE READ 未必防止幻读,但 InnoDB 的实现能避免幻读(详见后文)。
  4. SERIALIZABLE

    • 最强隔离,通过对读取操作加锁(将读取当成加锁操作)达到串行化执行效果,从而防止所有三种现象,但并发性能最低。

四、InnoDB 的事务实现核心(重点)

InnoDB 是 MySQL 的主流事务型引擎,其实现关键点:MVCC(多版本并发控制)undo logredo 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 UPDATEUPDATEDELETE 会对匹配的行加排他锁(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),从而强制串行化执行。性能代价高。

六、举例说明(SQL 示例与并发行为)

假设表 t(a INT PRIMARY KEY, v INT)

  1. 脏读示例(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(脏数据)
  2. 不可重复读示例(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(不可重复读)
  3. 幻读示例(区别 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)。实际细节有时依赖于是否是一致性读还是锁定读。

  4. SELECT ... FOR UPDATE 与锁行为

    • SELECT * FROM t WHERE id=1 FOR UPDATE; 会对 id=1 记录加排他锁,阻止其他事务更新/删除该行,其他事务的普通 SELECT(非锁定读)仍可通过 MVCC 读到上一个快照版本(不被阻塞),但 UPDATE 将被阻塞直到锁释放。

七、事务性能、调优与常见陷阱

  1. 选择合适的隔离级别

    • 如果业务能容忍更弱的一致性(例如只需防止脏读),使用 READ COMMITTED 可以减少锁冲突并提升并发性。
    • 对写密集型、高并发场景,REPEATABLE READ 可能产生更多 gap locks,需权衡。
  2. 避免长事务

    • 长事务会阻止 purge 清理 undo,造成 undo 表空间增长、缓冲池压力,影响性能与磁盘空间。
  3. 合理使用索引

    • InnoDB 的行锁锁在索引上,缺少合适索引会导致全表扫描并触发表级锁(或大量锁),并发性能差。
  4. 尽量短小、少交互的事务

    • 减少事务中等待输入或长时间计算的步骤,避免持有锁时间过长。
  5. 处理死锁

    • 捕获并重试被死锁回滚的事务(应用层逻辑),使用 SHOW ENGINE INNODB STATUS; 查找死锁原因。
  6. 监控 undo/redo 与 buffer pool

    • 观察 innodb_undo_tablespaces, innodb_buffer_pool_size, redo log 大小(innodb_log_file_size),双写(doublewrite)策略等。
  7. 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_timeoutinnodb_flush_log_at_trx_commitinnodb_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 UPDATEUPDATEDELETE 等语句被其它事务加了锁。普通 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,并在应用层对死锁进行重试,是保持事务健壮性的关键。

学而不思则罔,思而不学则殆。