MySQL事务
事务是数据库保证数据正确性的核心机制。零基础可以先这样理解:
事务就是把一组 SQL 当成一个整体:要么全部成功,要么全部失败。
如果没有事务,转账、下单扣库存、支付改订单状态这类操作就会出现“执行到一半”的中间状态。例如 A 给 B 转账,A 的余额扣了,但 B 的余额没加,系统数据就坏了。
事务为什么重要
事务解决的是并发和失败带来的数据不一致问题:
| 问题 | 没有事务会怎样 | 事务如何解决 |
|---|---|---|
| 执行一半服务宕机 | 一部分 SQL 生效,一部分没生效 | 回滚未完成事务 |
| 多个用户同时修改 | 后提交的数据覆盖先提交的数据 | 锁和 MVCC 控制并发 |
| 读取到别人未提交数据 | 业务基于脏数据做判断 | 隔离级别限制可见性 |
| 查询过程中数据变化 | 同一事务前后结果不一致 | Read View 或锁保证一致性 |
ACID 原理图
flowchart TD
A["事务开始 BEGIN"] --> B["执行多条 SQL"]
B --> C{"是否全部成功"}
C -- "成功" --> D["COMMIT 提交"]
C -- "失败" --> E["ROLLBACK 回滚"]
D --> F["redo log 保证持久性"]
D --> G["undo log 支持回滚和 MVCC"]
D --> H["锁 / MVCC 保证隔离性"]
E --> G
F --> I["数据库状态保持一致"]
G --> I
H --> IACID 不是四个孤立概念,它们依赖 InnoDB 的日志、锁和 MVCC 一起实现:
| 特性 | 含义 | InnoDB 主要依赖 |
|---|---|---|
| Atomicity 原子性 | 要么全成功,要么全失败 | undo log |
| Consistency 一致性 | 事务前后数据满足约束 | 业务规则、约束、日志、锁共同保证 |
| Isolation 隔离性 | 并发事务之间互不随意影响 | 锁、MVCC、隔离级别 |
| Durability 持久性 | 提交后宕机也不丢 | redo log、刷盘策略 |
隔离级别和问题关系图
flowchart TD
A["并发事务"] --> B{"隔离级别"}
B --> C["读未提交\n可能脏读"]
B --> D["读已提交\n避免脏读"]
B --> E["可重复读\n避免不可重复读\nInnoDB 通过 MVCC + Next-Key Lock 缓解幻读"]
B --> F["串行化\n强制排队,隔离最强"]
C --> G["并发最高\n一致性最弱"]
F --> H["一致性最强\n并发最低"]选择隔离级别就是在一致性和并发性能之间做权衡。隔离级别越高,读写互相影响越多,吞吐量通常越低。
事务隔离级别及引发的问题
读未提交(Read Uncommitted)
允许读取尚未提交的数据变更。可能导致:
- 脏读:一个事务读取到另一个事务尚未提交且最终可能被回滚的数据。
- 不可重复读:同一事务内,多次读取同一数据时,结果可能因其他事务的提交而发生变化。
- 幻读:同一事务内,多次执行相同的查询,结果集中由于其他事务插入新数据,出现了之前不存在的行(对行的增删影响)。
读提交(Read Committed)
只允许读取已提交的数据。避免了脏读:一个事务不会看到其他事务未提交的数据。仍可能:
- 不可重复读:事务在执行过程中,若其他事务提交了对数据的修改,该事务再次读取时会得到不同的结果。
- 幻读:虽然每次读取都只看到已提交的数据,但在范围查询中,其他事务的插入操作可能导致前后两次查询结果集不一致。
可重复读(Repeatable Read)
在同一事务内,多次读取相同数据时,总是返回第一次读取时的数据版本,即使其他事务在此期间对这些数据进行了修改并提交。 InnoDB通过MVCC(多版本并发控制)机制实现该隔离级别,避免了不可重复读。仍可能:
- 幻读:在当前事务的执行过程中,其他事务插入了满足其查询条件的新数据,导致前后两次同样的范围查询结果集大小不同。InnoDB通过间隙锁(Next-Key Locking)策略在一定程度上缓解幻读问题,但并非完全消除。
串行化(Serializable)
最高级别的隔离,提供严格的事务顺序执行效果。完全避免脏读、不可重复读和幻读。
实现方式:通过在读取数据时获取更严格的锁(如范围锁),或者使用更为保守的策略,使得并发事务之间相互阻塞,从而实际上将并发操作转化为串行执行,以确保事务之间互不影响。
从低到高,隔离级别依次提高了对读数据的一致性保证,但同时也可能降低了系统的并发性能,因为更高的隔离级别通常需要更严格的锁定机制或复杂的并发控制技术。实际应用中,应根据业务需求和对数据一致性的要求选择合适的隔离级别,平衡数据安全性与系统效率。
问题模拟
准备
表字段:
| id | name | balance |
|---|---|---|
| 1 | 张三 | 0.00 |
| 2 | 李四 | 600.00 |
| 3 | 王五 | 300.00 |
常用命令:
sqlshow variables like 'transaction%';
Variable_name Value transaction_alloc_block_size 8192 transaction_allow_batching OFF transaction_isolation REPEATABLE-READ transaction_prealloc_size 4096 transaction_read_only OFF transaction_write_set_extraction XXHASH64 sqlSELECT @@GLOBAL.transaction_isolation, @@GLOBAL.transaction_read_only; SELECT @@SESSION.transaction_isolation, @@SESSION.transaction_read_only;sql-- 设置隔离级别命令 SET [GLOBAL | SESSION] TRANSACTION transaction_characteristic [, transaction_characteristic] ... transaction_characteristic: { ISOLATION LEVEL level | access_mode } -- 级别 level: { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE } access_mode: { READ WRITE | READ ONLY }
脏读
开启一个会话M修改隔离级别为读未提交,开启事务m并执行修改id为1的余额加500的操作。
sql-- 设置隔离级别-读未提交 SET SESSION TRANSACTION isolation level READ UNCOMMITTED; -- 开始事务 BEGIN; -- 修改 UPDATE user_balance SET balance = balance + 500 WHERE id = 1;再开启一个会话N也修改隔离级别为读未提交,开启事务n并执行查询id为1的数据操作。
sql-- 设置隔离级别-读未提交 SET SESSION TRANSACTION isolation level READ UNCOMMITTED; -- 开始事务 BEGIN; -- 查询 SELECT * FROM user_balance WHERE id = 1
上面N会话查询到id为1的余额增加了500。如下:
| id | name | banlance |
|---|---|---|
| 1 | 张三 | 500.00 |
此时假设M会话出现了异常执行了事务回滚操作,但是N会话已经读取到了余额500。
-- 回滚
ROLLBACK;综上:这就是事务n读取到了事务m未提交的数据即事务n读到了脏数据。若要避免此类问题那么数据库的隔离级别就要高于读未提交(READ UNCOMMITTED)
不可重复读
开启一个会话M修改隔离级别为读未提交,开启事务m并执行修改id为1的余额加500的操作。
sql-- 设置隔离级别-读已提交 SET SESSION TRANSACTION isolation level READ COMMITTED; -- 开始事务 BEGIN; -- 修改 UPDATE user_balance SET balance = balance + 500 WHERE id = 1;再开启一个会话N也修改隔离级别为读未提交,开启事务n并执行查询id为1的数据操作。
sql-- 设置隔离级别-读已提交 SET SESSION TRANSACTION isolation level READ COMMITTED; -- 开始事务 BEGIN; -- 查询 SELECT * FROM user_balance WHERE id = 1
此时事务m在未提交前,事务n是读取不到事务m修改的数据的。
事务m提交事务。
sql-- 提交 COMMIT;事务n执行查询可以查到事务m修改的数据。
sql-- 查询 SELECT * FROM user_balance WHERE id = 1在会话M中再开启一个事务w再次执行修改id为1的余额加500的操作,并提交事务。
sql-- 开始事务 BEGIN; -- 修改 UPDATE user_balance SET balance = balance + 500 WHERE id = 1; -- 提交 COMMIT;事务n再次执行查询也可以查到事务w修改的数据
sql-- 查询 SELECT * FROM user_balance WHERE id = 1
综上就可以得到一个问题,就是在一个事务n中的多次读取数据得到的结果不一样,这就是不可重复读,所以要保证在并发事务下不出现此问题,那么隔离级别就不能低于读已提交(READ COMMITTED)。
幻读
幻读是同一事务两次按同一范围条件查询,结果集合中的行数发生变化,例如第一次没有 balance >= 1000 的记录,另一个事务插入并提交后,第二次多出一行。
在 InnoDB 的 RR 下要区分:
- 普通
select是快照读,通常通过同一个 Read View 避免看到之后提交的新行。 select ... for update、update、delete是当前读,要读取最新版本,并通过记录锁、间隙锁或临键锁保护范围。- “RR 完全不存在任何幻读现象”过于绝对。混用快照读和当前读、业务先查后写时,仍要基于具体语句分析。
两个会话可这样观察当前读的范围保护:
-- 会话 A
set session transaction isolation level repeatable read;
start transaction;
select *
from user_balance
where id between 10 and 20
for update;-- 会话 B:若索引和锁范围满足条件,可能等待会话 A
insert into user_balance(id, name, balance)
values (15, '测试用户', 1000.00);锁范围会受到索引、条件、隔离级别、唯一性和执行计划影响,不能只按 SQL 文本猜测。
RC 与 RR 的本质差异
| 对比 | Read Committed | Repeatable Read |
|---|---|---|
| 普通快照读 | 通常每条语句创建新的 Read View | 通常事务第一次一致性读创建,后续复用 |
| 同事务两次普通查询 | 可能看到期间已提交的新版本 | 通常保持一致快照 |
| 范围锁 | 一般更少使用间隙保护,部分约束检查除外 | 当前读常结合 Next-Key Lock 防止范围插入 |
| 并发与一致性 | 锁冲突相对少,读视图更新 | 读一致性更强,但范围锁冲突可能更多 |
事务边界怎么设计
一个事务应该覆盖“必须一起成功或失败”的最小数据库操作集合:
校验必要状态 → 修改核心记录 → 写本地流水/事件 → 提交不要在事务中执行耗时 RPC、发送邮件、上传文件或等待人工输入。远程调用无法被本地数据库事务真正回滚,还会延长锁持有时间。跨系统一致性通常使用幂等、Outbox、事务消息、补偿或分布式事务方案。
保存点与自动提交
start transaction;
update account set balance = balance - 100 where id = 1;
savepoint after_debit;
insert into audit_log(content) values ('transfer');
rollback to savepoint after_debit;
commit;autocommit=1 时每条独立 DML 通常是一个事务;显式开启事务后,要在所有异常路径提交或回滚。连接归还连接池前若事务状态未清理,可能污染后续请求。
常见并发异常怎么处理
| 问题 | 示例 | 常见措施 |
|---|---|---|
| 丢失更新 | 两个请求读同一余额后分别覆盖 | 条件更新、版本号乐观锁或 for update |
| 超卖 | 先查库存再无条件扣减 | update ... where stock >= ? 并检查影响行数 |
| 重复支付回调 | 同一事件多次到达 | 业务唯一键、状态机条件更新、幂等记录 |
| 写偏差 | 两行分别满足约束,并发后整体约束失效 | 合适锁定共同约束点、串行化或重新建模 |
| 死锁 | 不同事务反序加锁 | 固定顺序、缩短事务、命中索引、有界重试 |
高频面试题
ACID 分别依赖什么
原子性主要依赖 undo 和事务机制;持久性主要依赖 redo 与刷盘策略;隔离性依赖锁和 MVCC;一致性是最终目标,由原子性、隔离性、持久性、数据库约束和正确业务逻辑共同保证,不能简单归给某一个日志。
RR 能完全解决幻读吗
InnoDB RR 的快照读通过一致 Read View 避免普通查询看到后提交的新行,当前读通过 Next-Key Lock 等保护索引范围。但回答必须说明快照读与当前读的区别,混用两者或条件未正确命中索引时仍要具体分析,不能只背“RR 没有幻读”。
乐观锁和悲观锁怎么选
悲观锁先锁定再修改,适合冲突高且事务短的关键资源;乐观锁用版本条件更新,冲突时重试,适合读多写少。选择要看冲突率、重试成本、事务长度和业务容忍度,不是固定谁更快。
本章检查清单
- 能用两个会话复现脏读、不可重复读、锁等待和死锁。
- 能解释 RC 与 RR 的 Read View 创建差异。
- 能区分快照读、当前读和加锁范围。
- 能为库存扣减、支付回调设计正确事务边界。
- 能说明长事务对锁、undo、purge、复制和回滚的影响。
