数据库事务详解:以 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 解决读写冲突、用锁解决写写冲突,二者配合,才让高并发下既能保证正确,又不至于把性能拖垮。