9.1 事务与 ACID:让一批改动同生共死


9.1 事务与 ACID:让一批改动同生共死

本节摘要:事务把多条语句绑成一个"要么全成、要么全不算"的执行单元,ACID 是它的四个承诺——原子性(全有或全无)、一致性(约束不被破坏)、隔离性(并发互不干扰)、持久性(提交即落盘)。回滚依赖"旧值先记账"的日志机制;SAVEPOINT 支持长事务的部分回滚;隔离级别在"隔离彻底"与"并发性能"之间取舍。本节把第 2 章那口后悔药补成完整理论。

一、转账中途断电之后

账户 A 扣款 100 元的 UPDATE 成功执行,紧接着给账户 B 入账的 UPDATE 刚要执行,机房断电。重启后数据库里必须有明确的答案:这笔转账算还是不算?没有事务的世界里,"扣款成功、入账丢失"就是可能的状态——钱凭空消失。事务给出的承诺是原子性:BEGIN 到 COMMIT 之间的所有改动是一个不可分割的单元, COMMIT 执行前发生任何意外(断电、报错、主动 ROLLBACK),已执行的改动全部作废。完整演练:

-- 转账事务:两步改动绑成一个单元 START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- A 扣款 UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- B 入账 -- 此刻在会话里查询 能看到改动 但其他连接看不到(隔离性) COMMIT; -- 两步一起落盘 对外可见
Query OK, 1 row affected (0.01 sec) Query OK, 1 row affected (0.01 sec) Query OK, 0 rows affected (0.03 sec) -- commit 回执

数据库凭什么能在断电后"反悔"?机制藏在执行顺序里:每条 UPDATE 真正改数据页之前,先把"改哪个位置、旧值是什么"写进重做日志(redo/undo log)。ROLLBACK 与崩溃恢复都靠这份账本:undo 记录让已执行的改动逐条倒放回去,redo 记录让已提交的改动在重启后重演。"先写日志、后改数据"(WAL,先写日志原则)是几乎所有关系型数据库的共同骨架。

ACID 四特性逐个拆

原子性(Atomicity)——全有或全无。上面的转账案例即证明。它的价值在失败场景:批处理跑到第 99 条失败,前 98 条自动作废,不会留下半成品状态。

一致性(Consistency)——约束永远不被破坏。账户总余额转账前后都是 1000 元;库存列有 UNSIGNED 约束,扣成负数的 UPDATE 直接报错回滚。一致性是目的,原子性、隔离性、持久性加上第 1 章的约束体系是手段。

隔离性(Isolation)——并发事务互不干扰。两个会话同时转账,谁也看不见对方未提交的中间态。上面的会话里"自己能看见、别人看不见",就是隔离性最直观的体感。彻底隔离代价高,于是有隔离级别的取舍(本节末尾展开)。

持久性(Durability)——COMMIT 即落盘。回执返回的瞬间,改动已经写入磁盘日志,之后断电也不丢。这是数据库对"提交"二字的全部含义。

特性 一句话 缺失的后果
原子性 全有或全无 半成品数据:钱扣了没到账
一致性 约束不被破坏 总账对不平、负库存出现
隔离性 并发互不干扰 读到别人的半成品中间态
持久性 提交即落盘 客户收到成功回执 数据却丢了

SAVEPOINT:长事务里的检查点

批处理跑十分钟,中途某一批数据有问题,想"撤销最近一段、保留前面成果"——ROLLBACK 一刀切会把全部改动抹掉。SAVEPOINT 在事务内部打桩,支持部分回滚:

-- 保存点:分阶段提交风险 START TRANSACTION; UPDATE books SET stock = stock - 1 WHERE id = 1; SAVEPOINT after_first; -- 打桩:第一批完成 UPDATE books SET stock = stock - 1 WHERE id = 999; -- 目标不存在 -- 影响 0 行 但假设后续逻辑因此异常 想撤销第二段 ROLLBACK TO after_first; -- 只撤销到桩 后面的作废 前面的保留 SELECT stock FROM books WHERE id = 1; -- 第一批改动还在 COMMIT; -- 最终只落第一批
Query OK, 0 rows affected (0.00 sec) -- SAVEPOINT 建立 Query OK, 0 rows affected (0.01 sec) -- ROLLBACK TO 回到桩 +-------+ | stock | +-------+ | 11 | -- id=1 的改动保留(从 12 减到 11) +-------+

注意 SAVEPOINT 不是"小事务":桩到 COMMIT 之间仍是同一个事务,最终要么整体提交要么整体回滚,桩只是事务内部的撤销刻度。

隔离级别:隔离与性能的四档取舍

并发事务的三个经典读异常,与四个标准隔离级别(弱到强)的对应:

隔离级别 脏读 不可重复读 幻读 说明
读未提交 可能 可能 可能 能读到别人未提交的改动,几乎不用
读已提交 杜绝 可能 可能 各库默认(PostgreSQL、Oracle)
可重复读 杜绝 杜绝 可能* MySQL 默认,*InnoDB 借间隙锁大幅缓解
串行化 杜绝 杜绝 杜绝 彻底排队,并发性能最差

三个异常各配一个场景:脏读——读到别人还没提交的"转账成功",转眼对方 ROLLBACK,你基于幻影数据做了决策;不可重复读——同一事务内两次读同一行,中间被别人改了,两次结果不同,报表前后对不上;幻读——两次同一条件的范围查询,第二次多出几行(别人插入了新行),"库存不少于十种"的判断瞬间失效。

-- 查看与设置隔离级别(MySQL 语法) SELECT @@transaction_isolation; -- 当前级别 SET SESSION transaction_isolation = 'READ-COMMITTED'; -- 本会话改为读已提交
@@transaction_isolation: REPEATABLE-READ -- MySQL 8 默认可重复读 Query OK, 0 rows affected (0.00 sec)

选型经验:默认级别先跑,出现具体异常再升档。金融对账类求稳用可重复读;高并发短事务的互联网业务常用读已提交换吞吐。隔离级别的锁细节连着第 10 章的性能话题——锁等得久,查询就慢,这里先立个接口。

⚠️ 常见坑:事务里混进 DDL。多数数据库执行 DDL 会隐式提交当前事务,前面的改动瞬间落盘、失去回滚资格——本想"试完再撤",一条 ALTER 把退路烧了。事务里只放 DML,结构变更放事务外单独执行。

💡 关键直觉:事务是"暂存区 + 账本"的组合:改动先进暂存区(别人看不见),账本随时记着怎么撤销(undo),COMMIT 才把暂存区盖章归档(redo 保证断电不丢)。三个部件对上号,ACID 不再是四个抽象字母。

本节要点回顾

  • ACID 四承诺:原子全有全无、一致约束不破、隔离并发不扰、持久提交即落盘;
  • 先写日志后改数据:undo 支持回滚、redo 支持崩溃恢复,WAL 是共同骨架;
  • COMMIT 前一切可撤销:断电、报错、ROLLBACK 都让整批作废;
  • SAVEPOINT 是事务内部的刻度:部分回滚,但桩间仍是同一事务;
  • 隔离级别四档:默认级别先跑、出现异常再升档,锁与性能的联动留给第 10 章;
  • 事务里禁止 DDL:隐式提交会烧掉退路。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U