“等等,连读取操作也会开启事务?”
我一直以为事务只是用来写数据的。直到我写了这样一行代码:
|
|
原来 SQLAlchemy 的 autocommit=False
(默认设置)会为任何数据库操作开启事务。哪怕只是查一下数据库,也会打开事务的大门。
为什么?因为像 PostgreSQL 这样的数据库希望你看到的是一个一致的“快照”——就像你在看照片时,这张照片不会变化。
图书馆类比:MVCC 解释
想象一个神奇的图书馆:
- 每个人都有自己的复印机
- 写作者会实际修改书本
- 读者总是拿到最新的复印件
当你读取(SELECT)时,你拿到的是复印件——永远不会阻塞别人。 当你写入(UPDATE/INSERT/DELETE)时,你必须拿到真正的书本,修改后再放回去。其他写同一本书的人必须排队等你。
这就是 MVCC(多版本并发控制)。PostgreSQL 会保留多份数据版本,这样读者永远不用等写者。
“那锁到底什么时候才会出现?”
下面这个时间线终于让我明白了:
|
|
只有当你的 UPDATE 真正落到数据库时,锁才会出现。如果你在修改数据后、提交前 sleep:
|
|
此时,其他人如果也想更新这个用户,就得等你这 10 秒。太痛苦了。
“我的事务会不会阻塞 500 万用户?”
只有当他们都在修改同一条数据时才会。想象一下办公隔间:
- 500 万人各自编辑自己的资料?没有排队。大家都在不同的隔间。
- 500 万人都在更新同一个计数器?那就得排长队了。
行级锁只影响操作同一行的人。不会影响整个数据库。
事务陷阱:只读代码也有坑
有个细节曾经坑了我:
|
|
虽然只是读取,事务却会持续 5 秒。对于小应用问题不大,但在大规模下可能会:
- 延迟数据库清理(vacuum)
- 不必要地占用资源
解决方法?让会话尽量短,或者读取后显式 rollback。
什么时候用 begin()
,什么时候只用 commit()
我经常遇到这个报错:
|
|
原来 async with db.begin():
会尝试开启一个新的事务。如果已经有自动开启的事务了,就会报错。
除非你 100% 确定当前没有事务,否则只用 await db.commit()
就够了。
终极事务速查表
时机 | 发生了什么 | 是否加锁 |
---|---|---|
SessionLocal() |
创建会话 | 无 |
第一次数据库查询 | 开启事务 | 只读无锁 |
UPDATE/DELETE |
行级锁 | 直到提交 |
await db.commit() |
写入磁盘,结束事务 | 全部释放 |
会话关闭 | 未提交自动回滚 | 全部释放 |
现场演示:亲眼看看锁的效果
可以在自己的数据库上试试:
|
|
运行时,打开 psql
,执行:UPDATE counter SET n = n + 1 WHERE id = 1;
你会发现它正好等了 10 秒。这就是锁的效果。
思维转变
之前:“事务是神秘的数据库魔法。” 现在:“事务只是用来保证数据一致性的时间边界,锁则保护共享资源。”
重点总结:
- 读取操作也会开启事务(autocommit=False 时)
- 写操作会对特定行加锁
- 锁会持续到 commit/rollback
- 保持事务简短,数据库才会开心
现在遇到查询卡住,我不会慌了——我会先查是谁在持有锁。这感觉真不错。