「扣了钱但没到账」——这类 bug 几乎都与事务有关。事务是数据库提供给应用的一层承诺:把一组操作打包成不可分割的整体。理解这层承诺的具体内容(ACID)和它的折扣(隔离级别),才能写出在并发下依然正确的代码。
事务解决什么问题
经典转账场景:A 给 B 转 100 元,需要两条 UPDATE:
UPDATE accounts SET balance = balance - 100 WHERE name = 'A';UPDATE accounts SET balance = balance + 100 WHERE name = 'B';如果第一条执行完系统宕机,钱就凭空消失了。用 START TRANSACTION 把两条语句包起来,数据库保证要么都生效,要么都不生效。这就是事务存在的意义。
ACID 四个承诺
| 特性 | 含义 | 靠什么实现 |
|---|---|---|
| 原子性 Atomicity | 事务内操作全做或全不做 | undo log(回滚日志) |
| 一致性 Consistency | 事务前后数据满足业务约束(如总额不变) | 由其他三个特性 + 应用代码共同保证 |
| 隔离性 Isolation | 并发事务互不干扰 | 锁与 MVCC |
| 持久性 Durability | 提交后的修改不因宕机丢失 | redo log(重做日志) |
其中「一致性」最容易被误解:它不是数据库单方面能保证的。比如「余额不能为负」这条约束,需要应用代码或 CHECK 约束配合,数据库只提供原子、隔离、持久这三块地基。
并发的代价:三种读异常
如果所有事务都严格串行执行,隔离性完美但吞吐量惨不忍睹。为了性能,数据库允许事务并发交错执行,于是产生了三种经典异常。可以用两个会话(Session 1 和 Session 2)亲手复现。
脏读(Dirty Read):读到了别的事务还没提交的修改。如果对方随后回滚,你读到的就是从未存在过的数据。
不可重复读(Non-Repeatable Read):同一事务内,两次读同一行结果不同——因为中间有别的事务修改并提交了它。
幻读(Phantom Read):同一事务内,两次按相同条件范围查询,第二次多出了「幽灵行」——别的事务插入并提交了符合条件的新行。
四种隔离级别:用正确性换性能
SQL 标准定义了四个隔离级别,逐级收紧:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不会 | 可能 | 可能 |
| REPEATABLE READ | 不会 | 不会 | 可能* |
| SERIALIZABLE | 不会 | 不会 | 不会 |
* MySQL InnoDB 在 REPEATABLE READ 下通过 Next-Key Lock 基本杜绝了幻读,这是它比 SQL 标准要求更严格的地方。
MySQL(InnoDB)默认 REPEATABLE READ,Oracle、PostgreSQL 默认 READ COMMITTED。从 MySQL 迁移到 PostgreSQL 的应用,要特别注意这个差异——同样的代码在 PG 下可能开始出现不可重复读。
查看与设置当前隔离级别:
-- 查看SELECT @@transaction_isolation;
-- 仅对下一个事务生效SET TRANSACTION ISOLATION LEVEL READ COMMITTED;START TRANSACTION;动手实验:观察不可重复读
准备数据后开两个终端:
-- 准备CREATE TABLE t (id INT PRIMARY KEY, val INT);INSERT INTO t VALUES (1, 100);-- 会话 1 -- 会话 2SET TRANSACTION ISOLATION LEVEL READ COMMITTED;START TRANSACTION;SELECT val FROM t WHERE id = 1; -- 返回 100 START TRANSACTION; UPDATE t SET val = 200 WHERE id = 1; COMMIT;SELECT val FROM t WHERE id = 1; -- READ COMMITTED 下返回 200 -- REPEATABLE READ 下仍返回 100COMMIT;把隔离级别换成 REPEATABLE READ 再跑一遍,会话 1 两次读取结果相同——InnoDB 的 MVCC 让事务看到的是「事务开始那一刻」的一致性快照。
实践建议
- 事务要短。长事务持有锁和快照,会阻塞其他事务、撑大 undo log。把耗时操作(远程调用、文件处理)挪到事务外。
- 默认级别通常够用。不要为了「更安全」随手调 SERIALIZABLE,它本质是把并发读写成串行,高并发下性能崩塌。
- 显式处理死锁。并发事务加锁顺序不一致会死锁,InnoDB 检测到后会杀掉一个事务。应用层应捕获死锁错误(MySQL 错误码 1213)并重试。
- 统一加锁顺序。所有代码路径按相同顺序更新多张表/多行,能消除大部分死锁。
延伸阅读
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 23 天
部分内容可能已过时