开始前,会这些就够了
- 知道提交和回滚
- 会执行简单 SELECT
遇到不熟悉的词,先看这 2 个解释
- 事务
- 把一组数据库操作放进同一个提交范围,让它们作为一件事被确认或撤销,并按数据库规则控制其他事务看到的变化;它帮助处理做到一半时的失败。
- 隔离级别
- 多个事务交错运行时,数据库规定它们何时能看到彼此的修改、允许出现哪些相互影响;隔离级别就是这些规则的选择,影响读取结果与并发处理方式。
假设你开两个数据库窗口:甲读到库存十件,乙改成二十件并提交,甲再查仍然是十件。这不一定是缓存,也不一定是提交失败。甲可能仍处于一个读取旧快照的事务里。要解释它,除了问“有没有提交”,还要问“谁在什么时候开始读”。
回滚、可见性和业务正确是三件事
回滚关注本次修改失败后是否撤回;隔离关注同时进行的事务彼此能看到什么;业务正确还取决于约束和操作方式。把几句 SQL 放进事务,只解决了一部分问题,不能顺势推断它们会自动避开所有并发冲突。
快照是一套读取某个时间点数据版本的规则,不必真的复制整张表。读者可以继续看到旧版本,写者可以完成新版本;两者不矛盾。具体什么时候确定快照、下一条语句会不会换一份,要看数据库、隔离级别和语句类型。
只用本地文件观察一次快照
下面限定为 SQLite 的 WAL 模式,用两个连接访问同一个临时数据库。甲先开始事务并读取,乙更新后提交,甲在原事务里仍看到十;甲结束事务后再次读取,才看到二十。程序使用临时目录,退出时删除本次创建的练习文件,不接触已有数据库。
这个实验故意顺序安排两条连接的动作,不用线程制造随机结果。它展示的是 SQLite WAL 的已定义读取规则,不是 PostgreSQL 或 MySQL 所有隔离级别的模拟。观察点都放在提交前后,便于把结果与事务范围对应起来。
import sqlite3
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory(prefix="snapshot-demo-", dir=".") as folder:
path = Path(folder) / "demo.db"
a = sqlite3.connect(path, isolation_level=None)
b = sqlite3.connect(path, isolation_level=None)
try:
assert a.execute("PRAGMA journal_mode=WAL").fetchone()[0] == "wal"
a.execute("CREATE TABLE stock (id INTEGER PRIMARY KEY, qty INTEGER)")
a.execute("INSERT INTO stock VALUES (1, 10)")
a.execute("BEGIN")
print(a.execute("SELECT qty FROM stock WHERE id=1").fetchone()[0])
b.execute("BEGIN")
b.execute("UPDATE stock SET qty=20 WHERE id=1")
b.execute("COMMIT")
print(a.execute("SELECT qty FROM stock WHERE id=1").fetchone()[0])
a.execute("COMMIT")
print(a.execute("SELECT qty FROM stock WHERE id=1").fetchone()[0])
finally:
b.close()
a.close()
怎样运行:Python 3.10+ 标准库、SQLite 3.7.0+;在自己的空练习目录保存为 snapshot_demo.py,运行 python snapshot_demo.py。
运行后对照结果
10
10
20
换一个数据库,先核对规则
PostgreSQL 17 的 READ COMMITTED 会让普通 SELECT 在每条语句开始时取得快照,因此同一事务中的两次查询可能看到不同的已提交值。REPEATABLE READ 则让后续查询沿用事务中的那份快照,并且可能要求整个事务因并发冲突而重试。
MySQL 8.4 InnoDB 的 REPEATABLE READ 普通一致性读取,通常沿用第一次这类读取建立的快照;READ COMMITTED 每次一致性读取使用新的快照。自身已经做出的修改还存在可见性规则,UPDATE 和带锁查询也不能简单套用普通 SELECT 的结论。
排查时把事务边界写出来
遇到“看不到新值”,先记录连接身份、自动提交设置、隔离级别、第一次读取时间,以及另一方确实完成提交的证据。如果连接来自连接池,还要确认事务有没有在返回池前结束。仅截图最后一条 SELECT,通常不足以还原这段历史。连续点击两次查询按钮,也不一定处于同一事务:工具可能每次自动提交,也可能一直保留手动开启的事务,必须先确认它实际发了哪些语句。
需要立即根据库存做决定时,不要仅靠“先读一个数、在程序里减一、再写回”。应选择能表达业务条件的数据库操作,并检查受影响行数、约束或必要的锁。旧快照能解释读取结果,但并不自动替你保证扣库存规则。
记住这三点
- 事务同时影响修改能否撤回和读取能看见哪一版。
- 快照何时建立,必须结合产品、版本、级别和语句类型判断。
- 提交成功不代表所有仍在执行的旧事务立刻看见新值。
别把例子用过头
- SQLite WAL 实验不能代替
MySQL或PostgreSQL的并发测试。 - 普通读取、带锁读取和更新语句不一定使用相同的可见性规则。
- 读取稳定不等于业务规则自动安全,冲突处理仍要设计。
自己试一小步
如果甲在乙提交之前只执行 BEGIN、还没有读取,第一次读取结果应该怎样验证?
- 改变实验的动作顺序,只移动甲的第一次 SELECT。
- 保留每个 BEGIN、SELECT、UPDATE、COMMIT 的编号。
- 查所用数据库的固定版本文档,说明快照建立在哪一步。
做到这些,就算完成
- 结论注明数据库和模式。
- 不把事务开始时间一律当成快照建立时间。
- 把实际运行观察与对其他数据库的推断分开。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Isolation In SQLite (新标签页)↗
支持 WAL 的快照隔离、两个连接的读取示例、结束旧事务后读取新值和模式差别。
- PostgreSQL 17: Transaction Isolation (新标签页)↗
支持 READ COMMITTED 每语句快照、REPEATABLE READ 的快照与冲突重试。
- MySQL 8.4: Consistent Nonlocking Reads (新标签页)↗
支持
InnoDB一致性读取快照、自身修改的例外,以及普通读取与 DML 不同的条件。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
数据库管理系统与 HuaDB 实验 (新标签页)↗
已有事务基础后,只读多版本并发控制部分,用两个事务的读写时间线标出每次能看到的版本,先不做数据库内核实验。
查看学习准备与访问说明 →