1157 字
6 分钟
事务、ACID 与隔离级别:并发异常从何而来

「扣了钱但没到账」——这类 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 -- 会话 2
SET 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 下仍返回 100
COMMIT;

把隔离级别换成 REPEATABLE READ 再跑一遍,会话 1 两次读取结果相同——InnoDB 的 MVCC 让事务看到的是「事务开始那一刻」的一致性快照。

实践建议#

  1. 事务要短。长事务持有锁和快照,会阻塞其他事务、撑大 undo log。把耗时操作(远程调用、文件处理)挪到事务外。
  2. 默认级别通常够用。不要为了「更安全」随手调 SERIALIZABLE,它本质是把并发读写成串行,高并发下性能崩塌。
  3. 显式处理死锁。并发事务加锁顺序不一致会死锁,InnoDB 检测到后会杀掉一个事务。应用层应捕获死锁错误(MySQL 错误码 1213)并重试。
  4. 统一加锁顺序。所有代码路径按相同顺序更新多张表/多行,能消除大部分死锁。

延伸阅读#

事务、ACID 与隔离级别:并发异常从何而来
https://meow.us.kg/posts/database-transaction-acid-isolation/
作者
もゆちゃん
发布于
2026-09-29
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取