不能直接用普通字典模拟事务,因为写入不可逆且缺乏状态快照与惰性提交机制;需用contextmanager实现事务上下文,通过浅拷贝保存快照并在异常时恢复。

为什么不能直接用字典模拟事务
用普通 dict 做内存数据库时,一旦写入就不可逆——比如执行了 db["user"] = {"id": 1},后续出错也没法自动撤回。事务回滚本质是「状态快照 + 惰性提交」,不是简单备份整个 dict,否则并发或大体积数据时性能崩塌。
用 contextlib.contextmanager 实现事务上下文
Python 的 contextmanager 是最轻量、最贴近实际需求的方案:它天然支持 __enter__(保存快照)和 __exit__(异常时恢复),且无需继承类或重载魔术方法。
实操建议:
- 在
__enter__中对当前数据做浅拷贝(copy.copy())或深拷贝(仅当值含嵌套可变对象时才需copy.deepcopy()) - 把拷贝存为临时变量,不替换原数据;所有操作都作用于原数据,但回滚时用快照覆盖回去
-
__exit__中只判断exc_type是否为None:非 None 就还原,否则视为 commit 成功
示例关键片段:
立即学习“Python免费学习笔记(深入)”;
from contextlib import contextmanager
import copy
<p>class SimpleDB:
def <strong>init</strong>(self):
self._data = {}</p><pre class='brush:python;toolbar:false;'>@contextmanager
def transaction(self):
snapshot = copy.copy(self._data) # 浅拷贝足够,因值是 dict/list 等引用类型时需另处理
try:
yield self
except Exception:
self._data.clear()
self._data.update(snapshot)
raise嵌套事务怎么处理
纯内存实现里不推荐真嵌套(比如外层事务里再开一个 with db.transaction():),因为快照叠加会导致语义混乱:内层 rollback 只该影响内层修改,但浅拷贝无法自动分层。
更务实的做法是禁止嵌套,或用栈式快照管理:
- 维护一个
_snapshots列表,每次transaction()进入时 push 当前状态 -
__exit__时 pop 并还原,但必须确保每个with都有对应 exit,否则栈错位 - 若允许嵌套,务必在
__exit__中检查exc_type和exc_val,避免吞掉本该上抛的异常
哪些操作会破坏事务一致性
最容易被忽略的是「外部副作用」:事务内调用了 requests.post()、写文件、改全局变量,这些不会随回滚撤销。
还有两类隐性陷阱:
- 对值对象做原地修改:比如
db["config"]["timeout"] = 30—— 因为config是引用,快照里也指向同一 dict,回滚后该修改依然生效 - 使用
setdefault或get+ 赋值组合:它们可能触发意外写入,应统一走__setitem__或封装写方法 - 多线程共用一个
SimpleDB实例却没加锁:事务快照和写入不同步,会出现脏读
真正安全的写法是封装 update()、delete() 方法,在内部统一管控变更入口。
事务不是魔法,它只管你显式交给它的那部分数据状态。其余一切,得靠人盯住。


















