MySQL触发器无法捕获死锁异常且不支持重试,因InnoDB引擎级死锁中断会直接终止整个事务并抛错给客户端,触发器内DECLARE HANDLER对错误1213无效;替代方案是将逻辑移至应用层,用循环+异常捕获实现指数退避重试。

MySQL触发器里无法捕获死锁异常,也不能做重试。 这是InnoDB引擎和触发器机制共同决定的硬限制,不是写法或配置问题。
为什么触发器中不能捕获 Deadlock found when trying to get lock
触发器运行在服务器端事务上下文中,它没有独立的异常处理栈,也不支持TRY...CATCH(MySQL不提供该语法)、DECLARE HANDLER对死锁错误的响应无效。当你在触发器里执行UPDATE导致死锁时:
- 死锁被InnoDB检测到后,整个外部事务会被标记为“需回滚”,触发器随之终止
- 错误信息
Deadlock found when trying to get lock; try restarting transaction直接抛给客户端,触发器内部无从拦截 - 即便你在触发器开头写
DECLARE CONTINUE HANDLER FOR 1213 ...,它也不会触发——因为死锁不是标准SQLSTATE错误,而是引擎级中断
触发器中调用SELECT ... FOR UPDATE会放大死锁风险
很多开发者想在触发器里加行锁来“保证一致性”,结果适得其反:
- 触发器执行时机不可控(INSERT/UPDATE/DELETE时隐式触发),容易和主SQL形成交叉加锁顺序
- 例如:主SQL更新
orders表并触发after update,触发器再去SELECT stock FROM inventory WHERE id = ? FOR UPDATE,而另一事务正相反——高概率死锁 - InnoDB对触发器内语句同样施加间隙锁、临键锁,且无法通过降低隔离级别绕过
替代方案:把逻辑移出触发器,改到应用层做重试
真正可行的路径是放弃在触发器里处理并发冲突,改为由应用代码承担重试责任:
- 删掉触发器中所有带锁操作(尤其是
FOR UPDATE、UPDATE、DELETE) - 把原本放在触发器里的业务逻辑(如扣库存、记日志、发通知)提取成独立事务函数
- 在应用层用循环+捕获
DBAPIError(SQLAlchemy)或IntegrityError(PyMySQL)判断orig.args[0] == 1213或"Deadlock" in str(orig) - 示例片段(Python + SQLAlchemy):
def transfer_with_retry(session, from_id, to_id, amount): for i in range(3): try: session.execute("UPDATE accounts SET balance = balance - :amt WHERE id = :id", {"amt": amount, "id": from_id}) session.execute("UPDATE accounts SET balance = balance + :amt WHERE id = :id", {"amt": amount, "id": to_id}) session.commit() return except DBAPIError as e: if hasattr(e, 'orig') and e.orig and (e.orig.args[0] == 1213 or "Deadlock" in str(e.orig)): session.rollback() if i == 2: raise time.sleep(0.1 * (2 ** i)) # 指数退避 else: raise
最常被忽略的一点:触发器不是事务安全的“保险丝”,它是并发风险的放大器。哪怕你绕过死锁去捕获其他错误,也无法解决根本问题——锁顺序混乱、事务粒度失控、索引缺失导致的全表扫描加锁,这些都必须在SQL设计和应用流程层面收敛。


















