1. 什么是事务
事务(Transaction)是一组要么全部成功、要么全部失败的数据库操作。
以转账为例:
- A 账户扣 100
- B 账户加 100
这两步必须作为一个整体执行,不能只成功一半。
2. ACID 四大特性
- 原子性(Atomicity)
- 事务内操作不可分割,要么全成功,要么全回滚。
- 一致性(Consistency)
- 事务前后,数据库从一个一致状态转移到另一个一致状态。
- 隔离性(Isolation)
- 并发事务之间互不干扰。
- 持久性(Durability)
- 事务提交后结果持久保存。
InnoDB 常见实现对应关系(简化):
- 原子性:undo log
- 持久性:redo log
- 隔离性:MVCC + 锁
- 一致性:由原子性、隔离性、持久性共同保障
3. 并发异常:脏读、不可重复读、幻读
- 脏读
- 读到了别的事务未提交的数据。
- 不可重复读
- 同一事务中,两次读取同一行,结果不同。
- 幻读
- 同一事务中,两次范围查询,结果集行数变化(多了或少了"幻影行")。
4. 四种隔离级别
- 读未提交(Read Uncommitted)
- 允许脏读、不可重复读、幻读。
- 最低的隔离级别,事务可以读取其他事务尚未提交的数据,虽然拥有超高的并发处理能力及很低的系统开销,但很少用于实际应用,因为可能导致数据不一致性。
- 读已提交(Read Committed,RC)
- 避免脏读。
- 仍可能不可重复读、幻读。
- 事务只能读取已经提交的数据,避免了脏读问题,但可能导致不可重复读和幻读。这是大多数数据库系统的默认隔离级别(如 Oracle 和 SQL Server),但不是 MySQL 的默认。
- 可重复读(Repeatable Read,RR,InnoDB 默认)
- 避免脏读。
- 在快照读语义下避免不可重复读。
- 配合 next-key 锁可抑制很多幻读场景。
- 事务在整个事务期间保持一致的快照,其他事务的修改不会影响正在运行的事务,从而防止不可重复读问题。这是 MySQL InnoDB 默认的事务隔离级别。
- 串行化(Serializable)
- 隔离最强,但并发能力最低。解决所有事务并发问题。
- 最高的隔离级别,通过强制事务排序,使之不可能相互冲突,从而防止所有并发问题。虽然这个隔离级别可以解决上面提到的所有并发问题,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。最直观的体现就是,当数据库隔离级别设置为串行化后,A 事务在未提交之前,B 事务对 A 事务数据的操作都会被阻塞。通常不会使用这个隔离级别,我们需要其他机制来解决这些问题:比如乐观锁和悲观锁。
快速对照(经典结论):
严重级别
隔离水平高低

针对不同的隔离级别,并发事务可能发生的现象也会不同。

5. 先讲最容易混淆的一点:RC 和 RR 的"共同点"
你在对话里反复追问的核心非常关键:
"行锁是不是事务结束才释放?"
标准答案:
- 对于 UPDATE、DELETE、SELECT ... FOR UPDATE 这类当前读写操作,InnoDB 的行级排他锁通常都要等到 COMMIT 或 ROLLBACK 才释放。
- 这点在 RC 和 RR 下都成立。
所以:
- 同一行并发 UPDATE,不会并行成功。
- 谁先拿到锁谁先执行,其他事务阻塞等待。
此外,下面这个场景也要牢记:
- UPDATE t SET c = c + 0 WHERE id = 1
即使值"看起来没变",它仍然是写操作,仍要参与锁竞争。
6. RC 和 RR 的"真正区别"
如果写锁释放时机一样,那差异在哪里?
核心就两条:
- 快照读(普通 SELECT)生成 Read View 的策略不同
- RC:每条语句都可能生成新的 Read View
- RR:事务内一致性读通常复用同一个 Read View
- 范围锁策略不同
- RR 更积极使用 next-key lock(记录锁 + 间隙锁)抑制范围幻读
- RC 下间隙锁行为更弱(一般场景不如 RR 强)
一句话记忆:
- 写冲突层面:RC 和 RR 很像
- 读一致性与防幻读层面:RC 和 RR 差异明显
7. MVCC:为什么普通查询不一定阻塞写
MVCC(多版本并发控制)让"读"和"写"在很多场景下能并行:
- 行记录有版本信息(可理解为 trx_id + undo 版本链)
- 读取时根据 Read View 判断哪个版本对当前事务可见
关键结论:
- 普通 SELECT(快照读)多数情况下不加行锁
- UPDATE / DELETE / SELECT ... FOR UPDATE 属于当前读,会加锁
这就是"有时不阻塞、有时阻塞"的根本原因。
8. 多并发案例
下面用同一张表演示:
sql
CREATE TABLE account (
id INT PRIMARY KEY,
balance INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO account(id, balance) VALUES (1,100), (2,100)
ON DUPLICATE KEY UPDATE balance=VALUES(balance);
案例 A:RR 下,T1 先 A+10 再 B-10,T2 执行 B+0
事务定义:
- T1:先改 id=1(A),再改 id=2(B)
- T2:改 id=2(B+0)
时序:
- T1: BEGIN
- T1: UPDATE id=1
- T1: UPDATE id=2
- T2: BEGIN
- T2: UPDATE id=2(阻塞)
结论:
- T2 被阻塞直到 T1 提交/回滚
- B+0 也会参与锁竞争
案例 B:RR 下交叉更新(经典死锁)
事务定义:
- T1:A + 10,再 B - 10
- T2:B + 10,再 A - 10
典型交错:
- T1 锁住 A
- T2 锁住 B
- T1 请求 B,等待
- T2 请求 A,等待
- 循环等待,死锁检测触发,MySQL 回滚其中一个事务
关键结论:
- 先遇到的是死锁,不是"提交顺序影响结果"
- 死锁分支下,提交顺序问题失去意义
案例 C:同一行三事务更新,为什么会像"按顺序覆盖"
初始 C=10,三个事务都改 C:
- T1: C = C - 10
- T2: C = C + 10
- T3: C = C * 10
真实过程:
- 三者不是并发同时改成功,而是排队抢同一把行锁
- 前一个提交后,下一个才继续执行
- 后一个基于"最新已提交值"计算
所以"按顺序覆盖"不是无锁并行,而是锁竞争后的串行化效果。
案例 D:RC 与 RR 在普通 SELECT 的差异
假设事务 X 中两次普通 SELECT 同一行:
- RC:第二次可能看到别的事务刚提交的新值
- RR:第二次通常仍看到旧快照值(可重复读)
案例 E:范围查询与幻读
RR 下锁定读:
sql
SELECT * FROM account WHERE id BETWEEN 1 AND 10 FOR UPDATE;
在合适索引条件下,可能触发 next-key 锁,抑制该范围插入导致的幻读。
9. 对话中一个常见误区的纠正
误区说法:
- "RC 下交叉更新不会死锁,RR 才会死锁"
更准确说法:
- 死锁根因是加锁顺序冲突,不是隔离级别名字本身。
- 在同样的锁顺序反转场景下,RC 也可能死锁。
- RR 之所以经常被提到,是因为范围锁策略更强,锁冲突场景更多。
10. 如何在业务里降低死锁与事务冲突
- 固定更新顺序
- 多资源更新统一顺序,例如统一按主键升序。
- 缩短事务
- 减少事务中非必要逻辑,缩短持锁时间。
- 让 SQL 走索引
- 避免锁范围扩大。
- 做好重试机制
- 捕获 1213(死锁)和 1205(锁等待超时),幂等重试。
- 区分快照读与当前读
- 普通 SELECT 与 SELECT ... FOR UPDATE 语义完全不同。
11. 可直接复现的双会话脚本
会话 1:
sql
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
UPDATE account SET balance = balance + 10 WHERE id = 1;
-- 暂不提交
会话 2:
sql
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
UPDATE account SET balance = balance + 10 WHERE id = 1;
-- 此处阻塞
会话 1:
sql
COMMIT;
会话 2 将解除阻塞继续执行。
如果把两会话改成 A->B 与 B->A 顺序,就可快速复现死锁。
12. 一句话总复盘
- RC 和 RR 在"写锁释放时机"上基本一致,都是事务结束才释放。
- 两者真正区别在"快照读可见性规则 + 防幻读锁策略"。
- 并发更新看上去像"提交顺序决定结果",本质是锁队列导致的串行化执行。