事务就是「一组要么全成、要么全不成的语句」。
COMMIT 提交、ROLLBACK 回滚、SAVEPOINT 设存档点,三件套管住它。
最容易踩的坑是 DDL 隐式提交——
一句 CREATE TABLE 会把你前面没提交的改动先提交掉,之后回滚已经晚了。
4.6 讲的是「怎么改数据」。这一篇讲「改到一半不要了怎么办」。 从这一篇开始进第 13 章:数据库控制。 前面十二章基本是「一个人用库」,这一章开始讲「好几个人同时用会出什么事」。 事务是所有并发问题的地基,所以放在最前面。
一、ACID 四条,各管一段
| 字母 | 名字 | 一句话说清 | 靠什么实现 |
|---|---|---|---|
| A | 原子性 | 要么全成,要么全不成 | UNDO 回滚段 |
| C | 一致性 | 约束、外键不会破 | 约束本身 |
| I | 隔离性 | 互相看不见中间状态 | UNDO 快照 + 锁 |
| D | 持久性 | 提交了就不会丢 | 重做日志 + LGWR |
四条里最直观的是原子性:转账两条 UPDATE,不能只成功一条。
第二节说的 ROLLBACK 就是在实现它。
持久性靠的是重做日志——COMMIT 返回的那一刻,
改动已经写进日志文件了,而数据块本身可能还没落盘。
所以「断电了数据还在」靠的不是数据文件,是日志文件。
一致性其实不全是数据库的事。
数据库保证声明过的约束(主键、外键、CHECK)不被破坏。
但「总共 100 块,A 少了 30、B 就得多 30」这种业务规则,只有事务包住两条语句才能保证。
二、事务从哪开始,到哪结束
开始:第一条 DML 语句(INSERT/UPDATE/DELETE)自动开一个事务,
也可以显式写 SET TRANSACTION。
注意 SELECT 不开事务——只读不需要事务保护。
| 什么动作 | 事务怎么了 |
|---|---|
COMMIT |
结束,改动生效 |
ROLLBACK |
结束,改动全部撤销 |
执行 DDL(建表、改表、TRUNCATE、GRANT) |
先隐式提交,再执行 |
正常退出 SQL*Plus(EXIT) |
自动提交 |
| 会话异常终止、机器断电 | 自动回滚 |
表格里第四、五行常被忽略。
正常退出会提交,所以「跑完脚本忘了 COMMIT」通常没事——
但如果是程序崩溃或者被人 KILL,就变成回滚了。
这两种结局完全相反,所以别指望「退出时会帮你保存」。
三、三件套:COMMIT、ROLLBACK、SAVEPOINT
COMMIT:把改动定下来。
内部做三件事:写重做日志、释放锁、给其他会话发通知。
其中「释放锁」是并发能不能跑起来的关键——一个长事务占着行锁不放,别人全在排队。
ROLLBACK:把改动撤掉。
做法是拿 UNDO 里的旧值反向应用一遍。
所以回滚一个大事务,可能比执行它本身还慢。
SAVEPOINT:在事务里打一个存档点。
ROLLBACK TO s1 只退到 s1,事务不结束,后面的语句还能继续跑。
典型用法:一个事务里处理 100 条记录,每条之前打个存档点,某条失败就退到它之前,
跳过这条继续,最后统一 COMMIT。
注意 ROLLBACK TO 和 ROLLBACK 是两件事。
前者是「退一半」,后者是「全不要」。
写错一个 TO,就是「以为只撤了一条,其实全撤了」。
四、隔离级别:Oracle 只给你两档
| 级别 | Oracle 支持吗 | 能防什么 |
|---|---|---|
| READ UNCOMMITTED | 不支持 | Oracle 没有脏读这回事 |
| READ COMMITTED | 默认 | 脏读 |
| SERIALIZABLE | 支持 | 脏读、不可重复读、幻读 |
| READ ONLY | 支持(只读事务) | 整个事务看同一个快照 |
第一行是关键:Oracle 根本没有「脏读」这个级别。
别的数据库要显式设成 READ COMMITTED 才能避免读到别人没提交的数据,Oracle 默认就是。
这件事常被当成「Oracle 的怪癖」,其实是它的多版本机制决定的——
读的时候根本不看当前数据,看的是 UNDO 里的快照。
READ COMMITTED 的一致性只在【一条语句】内部有效。
一条语句跑 10 秒,这 10 秒里看到的是同一个快照;
但语句结束、下一条语句开始,就能看到别人新提交的数据了。
所以同一个事务里查两次,结果可能不一样——这不叫 bug,叫语句级一致性。
SERIALIZABLE 是事务级:整个事务一个快照。
代价是冲突时会报 ORA-08177(cannot serialize access),要整个事务重来。
所以只有「报表要对齐、账要对平」这种场景才用它,普通业务不用。
五、DDL 隐式提交:最常见的一个坑
下面这些语句执行前,Oracle 会先把当前事务提交掉:
CREATE/ALTER/DROP/TRUNCATE/RENAME/COMMENT,
以及 GRANT/REVOKE/ANALYZE/AUDIT。
后果很具体:
你写了一个脚本,先 INSERT 十万行,再 CREATE INDEX,最后 ROLLBACK。
你以为十万行都撤了,其实在 CREATE INDEX 那一刻就已经提交了。
再 ROLLBACK 什么也撤不掉。
这个坑最阴的地方是「没有报错」。
脚本一路绿灯跑完,数据却和你想的不一样。
所以规矩是:DDL 单独一个脚本,DML 单独一个脚本,中间不要混。
混着写省不了几行代码,排查要花一整天。
六、动手 Demo:转账、回滚、存档点,六个小实验
沙箱里没有 Oracle,但事务这件事 SQLite 做得和 Oracle 一样:开事务、提交、回滚、存档点,
语义完全对得上。
两个连接模拟两个人,能直接看出「未提交的数据别人看不见」。
唯一对不上的是 DDL:SQLite 的 CREATE TABLE 可以回滚,Oracle 的不行。
这一处会在输出里标出来,不拿它当共同结论。
# 事务:COMMIT / ROLLBACK / SAVEPOINT
import os, sqlite3, tempfile
p = os.path.join(tempfile.gettempdir(), "t58.db")
if os.path.exists(p):
os.remove(p)
a = sqlite3.connect(p, timeout=0)
a.isolation_level = None # 自己管事务
b = sqlite3.connect(p, timeout=0) # 第二个连接,模拟另一个人
b.isolation_level = None
a.execute("create table acct (id int primary key, bal real)")
a.executemany("insert into acct values (?,?)", [(1, 100.0), (2, 200.0)])
a.execute("begin")
a.execute("update acct set bal = bal - 30 where id = 1")
a.execute("update acct set bal = bal + 30 where id = 2")
print("A 自己看:", a.execute("select id, bal from acct").fetchall())
print("B 看到:", b.execute("select id, bal from acct").fetchall())
a.execute("commit")
print("提交后 B 看到:", b.execute("select id, bal from acct").fetchall())
a.execute("begin")
a.execute("savepoint s1")
a.execute("update acct set bal = bal - 10 where id = 1")
a.execute("savepoint s2")
a.execute("update acct set bal = bal - 10 where id = 2")
a.execute("rollback to s2") # 只退到 s2,s1 之后的保留
print("退到 s2 后:", a.execute("select id, bal from acct").fetchall())
a.execute("commit")
实跑输出:
--------------------------------------------------------------------
第五十八课:事务——提交、回滚、保存点
--------------------------------------------------------------------
**一条 UPDATE 要么全改,要么一行都不改,这一点靠事务保证。**
**这一课把 COMMIT/ROLLBACK/SAVEPOINT 三件套跑一遍。**
**再看两个「以为是这样、其实不是」:未提交的数据别人看不见,DDL 会隐式提交。**
一、转账:两条 UPDATE 必须同进退
--------------------------------------------------------------------
[Oracle 原码 · 本沙箱不执行]
| BEGIN
| UPDATE acct SET bal = bal - 30 WHERE id = 1;
| UPDATE acct SET bal = bal + 30 WHERE id = 2;
| COMMIT;
**A 自己看(事务里,还没提交):**
SQL> select id, name, bal from acct order by id
ID NAME BAL
-----------
1 张三 70
2 李四 230
已选择 2 行
**B 在另一个连接上看同一张表:**
SQL> select id, name, bal from acct order by id
ID NAME BAL
-----------
1 张三 100
2 李四 200
已选择 2 行
**B 看到的是 100 和 200,A 改的 30 块它一点都不知道。**
**这就是【读一致性】:没提交的修改只有自己看得见。**
**A 提交之后,B 再看:**
SQL> select id, name, bal from acct order by id
ID NAME BAL
-----------
1 张三 70
2 李四 230
已选择 2 行
二、ROLLBACK:改到一半不要了
--------------------------------------------------------------------
[Oracle 原码 · 本沙箱不执行]
| UPDATE acct SET bal = 0 WHERE id = 1;
| ROLLBACK;
**事务里改成 0,回滚之后再查,还是 70,一行没变:**
SQL> select id, bal from acct order by id
ID BAL
------
1 70
2 230
已选择 2 行
三、SAVEPOINT:只退一半
--------------------------------------------------------------------
[Oracle 原码 · 本沙箱不执行]
| SAVEPOINT s1;
| UPDATE acct SET bal = bal - 10 WHERE id = 1;
| SAVEPOINT s2;
| UPDATE acct SET bal = bal - 10 WHERE id = 2;
| ROLLBACK TO s2;
| COMMIT;
**退到 s2 之前:1 号 60、2 号 220:**
SQL> select id, bal from acct order by id
ID BAL
------
1 60
2 220
已选择 2 行
**ROLLBACK TO s2 之后:1 号的变化留着,2 号退回去了:**
SQL> select id, bal from acct order by id
ID BAL
------
1 60
2 230
已选择 2 行
**注意 ROLLBACK TO 不结束事务**,事务还开着,**后面还能接着改,最后仍要 COMMIT 或 ROLLBACK。**
四、DDL 会隐式提交:这条最容易埋雷
--------------------------------------------------------------------
[Oracle 原码 · 本沙箱不执行]
| INSERT INTO acct VALUES (3, '王五', 500);
| CREATE TABLE tmp1 (id INT); -- 这一句先把上面那句提交了
| ROLLBACK; -- 撤不掉 3 号了
教材原码,本沙箱不执行:SQLite 的 DDL 也在事务里,回滚得掉
**本沙箱实测:上面那段回滚之后,3 号也没了:**
SQL> select id, bal from acct order by id
ID BAL
------
1 60
2 230
已选择 2 行
**SQLite 的 CREATE TABLE 是可回滚的,Oracle 的不是。**
**所以这条只能在 Oracle 上验:DDL 之前没提交的 DML 会被 DDL 顺手提交,回滚时已经晚了。**
**实用结论:DDL 单独放一个事务,别和 DML 混在一起写。**
**混着写的脚本,中途报错时你根本说不清哪些改动留下了。**
五、一条语句本身也是原子的
--------------------------------------------------------------------
[Oracle 原码 · 本沙箱不执行]
| UPDATE u58 SET id = 9; -- 两行都改成 9,第二行撞主键
| -- 结果:整条语句回滚,两行都还是 1 和 2
**报错:UNIQUE constraint failed: u58.id**
SQL> select id from u58 order by id
ID
--
1
2
已选择 2 行
**第一行本来已经改成 9 了,报错之后又退回 1。**
**一条语句要么整条生效,要么整条不生效,没有「改了一半」这种状态。**
六、提交的代价:单独提交 vs 批量提交
--------------------------------------------------------------------
**500 行,一次提交:0.0150 秒;一行一次提交:5.9745 秒,慢 397.6 倍。**
**差距全在「每次提交都要把日志刷到磁盘」上。**
**Oracle 里对应的是 LGWR 写重做日志,同一个道理:提交次数才是成本,行数不是。**
**所以批量导入要「攒够一批提交一次」。**
**代价是中途失败时,最后没提交的那一批会丢。**
第一段:A 看到 70 和 230,B 看到 100 和 200。
同一张表、同一时刻,两个人看到的不一样——
这就是读一致性,也是最容易讲不清的一个概念:数据没有「唯一当前值」,
只有「你这个事务能看到的值」。
第二段:A 提交之后,B 再看就是 70 和 230 了。
中间那一瞬间发生了什么?
COMMIT 返回时日志已经落盘,数据块要等后面慢慢写。
所以「提交了但数据文件里还是旧值」是正常状态,不是没生效。
第三段:存档点。
退到 s2 之前是 60 和 220,退完之后是 60 和 230——1 号的减 10 留着,2 号的减 10 没了。
这就是「只退一半」,事务还开着。
第四段是这一篇的重点:DDL 那段在 SQLite 上回滚掉了,Oracle 上回滚不掉。
所以这个实验结果【不能】拿来推断 Oracle 的行为,两边的 DDL 事务语义不一样。
要验这一点只能在真库上试,本沙箱给不出答案。
第五段:一条 UPDATE 改成两行同一个值,撞主键报错,两行都退回原样。
语句级原子性,和事务级原子性是两回事,但都在这一层保护你。
第六段:一次提交和五百次提交,差几百倍。
差的全是「日志刷盘」这一次动作。
所以「批量操作慢」往往不是 SQL 慢,是提交次数太多。
30 秒实验
- 把
rollback to s2改成rollback to s1,再跑一次。 猜猜 1 号那 10 块还在不在? 答案是退到s1之后一起没了。 - 把「500 行一次提交」改成「每 100 行提交一次」,量一下总耗时。 它应该落在 0.01 秒和 5 秒之间的某个位置——这个位置就是「提交次数」和「耗时」的比例关系。
- 开两个连接,一个
begin之后只SELECT、不写,另一个也begin之后写。 看两边会不会互相挡。 结果是不挡——只读不开事务、也不加锁。
常见误区
- 「提交之后别人立刻能看到」。 大体是,但要等对方【下一条语句】开始。
正在跑的那条语句还看着旧快照,这是
READ COMMITTED的定义。 - 「
ROLLBACK能撤销任何操作」。 撤不掉已经COMMIT的,也撤不掉被 DDL 顺手提交过的。 - 「提交越频繁越安全」。 安全是对的,但慢,而且日志量翻倍。 正确做法是「攒一批提交一次」,批次大小按失败重做的成本来定。
- 「Oracle 有脏读级别」。 没有。
Oracle 不支持
READ UNCOMMITTED,任何隔离级别下都读不到未提交的数据。
自测三问(点开对答案)
1. `ROLLBACK TO s1` 和 `ROLLBACK` 差在哪? —— **前者只退到存档点 `s1`,事务还开着;后者整个事务结束,全部撤销。** 2. 为什么说「持久性靠日志不靠数据文件」? —— **`COMMIT` 时只保证日志落盘,数据块是延后写的。** **断电后重启,Oracle 拿日志把没写完的数据补上(前滚)。** 3. Oracle 的默认隔离级别是什么?它防住了什么? —— **`READ COMMITTED`。** 防住脏读,一致性范围是【一条语句】。**事务里查两次可能不一样。** 4. 为什么 DDL 要单独放一个脚本? —— **DDL 会隐式提交前面的 DML。** 混在一起写,中途失败时你说不清哪些改动留下了。下一篇讲锁。事务保证「要么全成要么全不成」,锁保证「两个人别同时改同一行」。 这两件事合起来,才是完整的并发控制。 重点是 Oracle 的锁粒度是【行】,而且读不加锁—— 这一条决定了它的并发能力比其他库高一个档次。
- ☐ (本篇完,下一篇 13.2 用锁控制并发存取)
相关笔记
- 操作数据表
- 触发器
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引