asyncio+aiomysql长事务易致MySQL死锁,因协程挂起延长锁持有时间且缺乏统一加锁顺序;应缩短事务、按主键升序操作、捕获1213错误重试,并优先用原子SQL或Redis预扣减规避。

asyncio + aiomysql 长事务为什么容易死锁
MySQL 死锁不是 Python 造成的,但 aiomysql 在异步场景下更容易暴露它:事务生命周期被协程挂起(比如 await 其他 IO),导致锁持有时间远超预期;多个协程交叉执行 UPDATE/SELECT FOR UPDATE,又缺乏明确的加锁顺序,就极易触发 Deadlock found when trying to get lock。
关键点在于:异步不等于“自动并发安全”,aiomysql 的连接池和事务管理仍依赖 MySQL 原生行为,协程切换不会自动释放行锁。
如何避免在 asyncio 中触发 MySQL 死锁
核心思路是缩短锁持有时间 + 统一加锁顺序 + 主动重试:
- 尽量把事务控制在单个
await块内,避免在START TRANSACTION和COMMIT之间穿插其他异步调用(如 HTTP 请求、缓存读写) - 对涉及多行更新的场景,始终按主键(或唯一索引)升序排序后再操作,例如:
UPDATE t SET x=1 WHERE id IN (5, 2, 8)改为WHERE id IN (2, 5, 8) - 捕获
aiomysql.Error并检查err.args[0] == 1213(MySQL 死锁错误码),最多重试 2–3 次,每次加小随机延迟(如await asyncio.sleep(0.01 + random.random() * 0.02)) - 避免在事务中使用
SELECT ... FOR UPDATE查询未索引字段,否则可能升级为表锁
aiomysql 事务重试的最小可靠写法
不要依赖装饰器或全局重试中间件——它们难以控制事务上下文。直接在业务逻辑里封装:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
async def update_user_balance(conn, user_id, delta):
for attempt in range(3):
try:
async with conn.cursor() as cur:
await cur.execute("SELECT balance FROM users WHERE id = %s FOR UPDATE", (user_id,))
row = await cur.fetchone()
if not row:
raise ValueError("user not found")
new_balance = row[0] + delta
await cur.execute("UPDATE users SET balance = %s WHERE id = %s", (new_balance, user_id))
await conn.commit()
return new_balance
except aiomysql.Error as err:
await conn.rollback()
if err.args[0] == 1213: # Deadlock
if attempt == 2:
raise
await asyncio.sleep(0.01 + random.random() * 0.02)
continue
raise
注意:必须在每次循环内新建 cur,且 conn.rollback() 要显式调用;用 async with conn.cursor() 自动清理游标,但不自动 rollback。
比重试更有效的预防手段
死锁重试只是兜底,真正降低概率要从设计入手:
- 把“查+改”合并为原子 SQL,例如用
UPDATE users SET balance = balance + %s WHERE id = %s替代先 SELECT 再 UPDATE - 对高频更新的计数类字段(如库存、积分),改用 Redis + Lua 做预扣减,MySQL 只做最终一致性落库
- 在应用层对同一业务实体(如 user_id)强制路由到同一个数据库连接(需定制连接池 key),避免跨连接竞争
- 开启 MySQL 的
innodb_deadlock_detect = ON(默认开启),但监控SHOW ENGINE INNODB STATUS中的 LATEST DETECTED DEADLOCK 日志,反向定位冲突 SQL 模式
死锁不是异常,而是并发设计的信号灯。最常被忽略的是:开发者以为 await 让出了 CPU 就等于让出了锁,其实锁一直攥在 MySQL 手里,直到 commit 或 rollback。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










