数据库事务详解:以 MySQL 为例

1. 什么是事务

事务(Transaction) 是一组要么全部成功、要么全部失败的数据库操作。它把多条 SQL 看作一个不可分割的逻辑单元——要么完整地提交,要么整体回滚,不会出现“做了一半”的中间状态。

典型场景:转账。从 A 账户扣钱、给 B 账户加钱,这两条 UPDATE 必须在一个事务里:

BEGIN;
UPDATE account SET balance = balance - 100 WHERE name = 'A';
UPDATE account SET balance = balance + 100 WHERE name = 'B';
COMMIT;

如果两条语句之间发生崩溃,事务未提交,MySQL 会回滚已执行的扣款,保证数据一致。

2. ACID 四大特性

特性 含义 MySQL 中的保障
Atomic 原子性 事务内操作要么全做、要么全不做 Undo Log(回滚日志)
Consistency 一致性 事务前后数据都满足业务约束 由原子性 + 隔离性 + 应用逻辑共同保证
Isolation 隔离性 并发事务互不干扰 锁 + MVCC + 隔离级别
Durability 持久性 提交后修改永久生效 Redo Log + 双写缓冲
flowchart LR
    A[事务] --> B[原子性 Undo Log]
    A --> C[一致性 约束+逻辑]
    A --> D[隔离性 锁+MVCC]
    A --> E[持久性 Redo Log]

一致性是“目标”,原子性、隔离性、持久性是“手段”。引擎保证 A/I/D,业务开发者负责 C 的约束正确。

3. MySQL 中的事务控制

MySQL 的存储引擎中只有 InnoDB 支持事务,MyISAM 不支持。

3.1 自动提交

MySQL 默认开启 autocommit,每条 SQL 自动包成一个事务立即提交:

-- 查看自动提交开关
SELECT @@autocommit;        -- 1 表示开启

-- 关闭后,需显式 COMMIT 才生效
SET autocommit = 0;

3.2 事务控制语句

BEGIN;                      -- 或 START TRANSACTION,开启事务
SAVEPOINT sp1;              -- 设置保存点
ROLLBACK;                   -- 回滚整个事务
ROLLBACK TO sp1;            -- 回滚到保存点(保存点之后撤销)
RELEASE SAVEPOINT sp1;      -- 删除保存点
COMMIT;                     -- 提交事务

SAVEPOINT 适合长事务里只回退部分操作的场景,避免一错全滚。

4. 并发带来的三大问题

当多个事务同时读写同一批数据时,若不隔离,会出现三类异常:

问题 描述 例子
脏读 读到别的事务未提交的数据,对方回滚后读到的是“脏”的 T1 改了值但未提交,T2 读到了;T1 回滚,T2 拿到无效值
不可重复读 同一事务内两次读同一行,结果不同(被别的事务修改/删除并提交) T1 读余额为 100;T2 改为 200 并提交;T1 再读变成 200
幻读 同一事务内两次范围查询,出现之前没有的“新行”(别的事务插入并提交) T1 查 age>20 有 3 条;T2 插入 1 条并提交;T1 再查变成 4 条

不可重复读侧重已有行被改/删,幻读侧重新行被插入,二者来源不同,解决手段也不同。

sequenceDiagram
    participant T1
    participant T2
    T1->>T1: BEGIN
    T2->>T2: BEGIN
    T2->>T2: UPDATE 余额=200 (未提交)
    T1->>T1: SELECT 余额 = 200 (脏读!)
    T2->>T2: ROLLBACK
    T1->>T1: 拿到已失效的 200 → 错误

5. 四种隔离级别

SQL 标准定义了 4 级隔离,级别越高越安全、并发越低。MySQL(InnoDB)默认是 REPEATABLE READ

隔离级别 脏读 不可重复读 幻读 说明
READ UNCOMMITTED(读未提交) ❌ 可能 ❌ 可能 ❌ 可能 啥都不保证,几乎不用
READ COMMITTED(读已提交) ✅ 避免 ❌ 可能 ❌ 可能 Oracle 默认,解决脏读
REPEATABLE READ(可重复读) ✅ 避免 ✅ 避免 ⚠️ 通常避免* MySQL 默认,靠 MVCC + Next-Key Lock
SERIALIZABLE(串行化) ✅ 避免 ✅ 避免 ✅ 避免 完全串行,性能最低

* MySQL 在 REPEATABLE READ 下通过 Next-Key Lock(间隙锁+记录锁) 在大部分场景避免了幻读,这是 MySQL 相对 SQL 标准的增强。

查看与设置隔离级别:

-- 查看当前会话 / 全局隔离级别
SELECT @@transaction_isolation;
SELECT @@global.transaction_isolation;

-- 设置当前会话为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

6. 隔离级别实战演示

以“不可重复读”为例,对比 READ COMMITTED 与 REPEATABLE READ:

会话 A(RC 级别,会被改):

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM account WHERE name = 'A';   -- 得到 1000
-- 此时会话 B 把 balance 改成 2000 并提交
SELECT balance FROM account WHERE name = 'A';   -- 得到 2000(不可重复读发生)
COMMIT;

会话 A(RR 级别,稳定快照):

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM account WHERE name = 'A';   -- 得到 1000
-- 会话 B 改成 2000 并提交
SELECT balance FROM account WHERE name = 'A';   -- 仍是 1000(快照读,可重复读)
COMMIT;

RR 下第二次读看到的是事务开始时的一致性快照,这正是 MVCC 的功劳。

7. 事务的实现原理

7.1 Redo Log(保证持久性)

InnoDB 写数据先写内存(Buffer Pool),再异步刷盘。若提交后还没刷盘就崩溃,已提交数据会丢。为此:

  • 事务提交时,先把修改顺序写入 Redo Log(顺序写、快),再返回成功;
  • 崩溃恢复时,用 Redo Log 重放,保证已提交事务不丢。

这就是 WAL(Write-Ahead Logging,预写日志):先记日志,再落数据。

7.2 Undo Log(保证原子性 + MVCC)

  • 回滚用:事务每改一行,旧值存进 Undo Log;回滚时按 Undo Log 反向还原。
  • 快照用:Undo Log 还保存了行的历史版本,MVCC 的“旧版本”就来自这里。

7.3 MVCC(多版本并发控制)

MVCC 让“读不加锁、读写不阻塞”:每行有隐藏的事务 ID(DB_TRX_ID)和回滚指针(DB_ROLL_PTR),指向 Undo Log 里的历史版本。

读时生成 ReadView(当前活跃事务快照),据此判断:

  • 版本的创建事务已提交且早于 ReadView → 可见;
  • 否则顺着 DB_ROLL_PTR 找上一个历史版本,直到找到可见的。
flowchart TD
    A[当前行 DB_TRX_ID] --> B{事务在 ReadView 中是否可见?}
    B -->|可见| C[返回该行]
    B -->|不可见| D[沿 DB_ROLL_PTR 找 Undo Log 历史版本]
    D --> B

RR 级别在事务第一次读时生成 ReadView 并复用,因此整个事务看到一致快照;RC 级别每次读都生成新 ReadView,所以能看到别的事务已提交的最新值。

8. 锁机制速览

InnoDB 默认行级锁,且锁是加在索引上的:

锁类型 作用范围 用途
记录锁(Record Lock) 单行 锁定具体索引记录
间隙锁(Gap Lock) 行之间的“空隙” 防止插入,解决幻读
Next-Key Lock 间隙 + 记录 RR 默认,左开右闭区间锁
意向锁(Intention Lock) 表级 快速判断表上是否有行锁

死锁示例与避免:

-- 会话 A 先锁 id=1,再想锁 id=2
BEGIN; UPDATE t SET v=1 WHERE id=1;
-- 会话 B 先锁 id=2,再想锁 id=1 → 互相等待,死锁
BEGIN; UPDATE t SET v=1 WHERE id=2;

避免死锁的经验:按固定顺序访问多行缩小事务粒度减少长事务。MySQL 检测到死锁会回滚其中一个事务并报 ERROR 1213

9. 实战中的常见坑

  • 长事务:长时间不提交会一直占用 Undo Log、阻塞 Purge 线程,导致历史版本堆积、回滚段膨胀。监控方式:

    SELECT * FROM information_schema.innodb_trx
    ORDER BY trx_started ASC LIMIT 5;   -- 看运行时间最长的事务
  • 在事务里做远程调用 / 睡眠:事务应“短平快”,把 RPC、HTTP 调用放到事务外面。

  • SELECT … FOR UPDATE 误用:会加排他锁,高并发下易成瓶颈,仅在确实需要“读后写”时用。

10. 小结

关注点 要点
本质 一组操作要么全成、要么全败
特性 ACID:原子/一致/隔离/持久
并发问题 脏读、不可重复读、幻读
隔离级别 MySQL 默认 RR,靠 MVCC + Next-Key Lock 兼顾性能与安全
持久性 Redo Log(WAL)
原子性 Undo Log 回滚
最佳实践 事务要短、顺序访问、避免长事务

理解事务,核心是理解“隔离性如何实现”——InnoDB 用 MVCC 解决读写冲突、用锁解决写写冲突,二者配合,才让高并发下既能保证正确,又不至于把性能拖垮。