必须显式使用async with session.begin()控制事务边界,配合for update行锁与死锁重试机制,才能保障异步数据库读写的数据一致性;仅依赖默认行为或asyncio.lock无法替代数据库原生事务与锁。

异步读写数据库时,数据一致性不能靠“运气”或“默认行为”保障,必须显式控制事务边界、锁策略和重试逻辑。
async with session.begin() 是事务安全的底线
在 SQLAlchemy 2.0+ 异步模式中,session.begin() 不只是语法糖——它强制启用事务上下文,异常时自动回滚,成功时自动提交。不用它,session.execute() 默认在 autocommit 模式下运行,每条语句独立提交,根本谈不上一致性。
常见错误现象:session.execute(...); session.execute(...) 连续调用后数据部分写入、部分丢失;或者中间抛异常导致前一条已生效、后一条未执行。
- 必须把所有关联操作包进同一个
async with session.begin():块内 - 避免在事务块里做 await asyncio.sleep()、HTTP 请求等非数据库操作
- 不要手动调用
session.commit()或session.rollback()—— 它们在begin()上下文中已被接管
FOR UPDATE 锁必须配合事务 + 显式查询顺序
单纯加 FOR UPDATE 不保证一致性:如果两个协程以不同顺序请求行锁(比如一个先查 user_id=100 再查 user_id=200,另一个相反),就可能形成死锁。而且,锁只在事务生命周期内有效。
使用场景:转账、库存扣减、计数器更新等需要“读-改-写”原子性的操作。
- 查询必须用
text("SELECT ... FOR UPDATE"),且确保该语句在session.begin()块内执行 - 多个行更新时,统一按主键升序访问(例如
WHERE id IN (100, 200)改为WHERE id IN (100, 200) ORDER BY id) - 避免在锁住 A 行后,再去查 B 表触发另一把锁——尽量单表、单事务、单顺序
死锁不是偶发事件,要主动捕获并重试
PostgreSQL 报 psycopg2.errors.DeadlockDetected、MySQL 报 1213: Deadlock found when trying to get lock,都不是该停服务的理由,而是设计上就该预期的信号。
性能影响:不重试会导致请求失败率陡增;盲目重试又可能放大竞争。关键在“指数退避 + 有限次数”。
- 用 try/except 包裹整个事务块,捕获具体死锁异常类(别用裸
except Exception) - 重试前
await asyncio.sleep(0.01 * (2 ** attempt)),最多 3 次 - 重试时必须新建
AsyncSession实例——旧 session 已处于不可用状态
异步锁(asyncio.Lock)只用于内存共享,不替代数据库锁
asyncio.Lock 能防止多个协程同时修改 Python 变量(如缓存计数器),但它对数据库无任何影响。协程被挂起时,数据库连接仍持有锁、事务仍在运行,反而延长了锁等待时间。
容易踩的坑:用 async with lock: 包裹数据库操作,误以为能防并发写;结果是锁住了 CPU,却没锁住数据库行,还拖慢了整体吞吐。
-
asyncio.Lock仅适用于纯内存临界区(如更新本地字典、生成唯一 ID) - 涉及数据库状态变更的操作,必须依赖数据库原生机制(事务 + 行锁 + 隔离级别)
- 若需跨进程一致性,
asyncio.Lock完全无效,得用 Redis 分布式锁或数据库 advisory lock
真正难的是权衡:隔离级别设太高(如 SERIALIZABLE)会大幅降低并发度;设太低(如 READ COMMITTED)又可能漏掉幻读。多数业务场景下,READ COMMITTED + 显式 FOR UPDATE + 死锁重试,才是务实的起点。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











