多实例Azure Function写入Azure SQL数据库抛出死锁异常如何解决
异常说明
Exception: ('40001', '[40001] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Transaction (Process ID 878) was deadlocked on lock | communication buffer resources with another process and has been chosen as the deadlock victim. Rerun the transaction. (1205) (SQLExecDirectW)')
死锁触发原因
- 资源循环等待:多个Azure Function实例同时操作SQL的关联数据,不同事务的加锁顺序不一致,比如实例1先锁定表A再申请表B的锁,实例2先锁定表B再申请表A的锁,双方互相等待对方释放资源触发死锁。
- 长事务放大冲突:单条Function请求对应的SQL事务包含过多操作、执行耗时过长,锁的持有时间被拉长,和其他实例的事务发生冲突的概率大幅提升。
- 锁范围过大:写入语句缺少必要索引支撑时会触发全表/大范围索引扫描,SQL会锁定远超过实际需要的行资源,不必要的锁覆盖范围提升了冲突概率。
- 隔离级别设置不合理:SQL默认的READ COMMITTED隔离级别就会持有共享锁直到查询结束,如果业务使用了更高的REPEATABLE READ、SERIALIZABLE隔离级别,锁的持有时间更长、覆盖范围更大,死锁风险更高。
- 并行执行内部冲突:如果写入语句使用了并行执行计划,同一条SQL的多个执行线程之间也可能出现通信缓冲区资源死锁,和跨实例的锁冲突叠加后死锁概率进一步提升。
排查方案
- 捕获死锁链路:通过Azure SQL的诊断功能开启死锁日志采集,或者查询
sys.dm_tran_deadlocks、sys.dm_os_wait_stats等系统视图,获取死锁涉及的SQL语句、锁资源类型、事务执行顺序,定位具体冲突点。 - 校验事务逻辑:梳理所有涉及写入的业务逻辑,确认不同事务的加锁顺序是否一致,是否存在可拆分的大事务。
- 检查执行计划:查看写入语句的执行计划,确认是否存在全表扫描、索引缺失等导致锁范围过大的问题。
- 查看数据库配置:确认当前SQL数据库的事务隔离级别、最大并行度等配置是否匹配业务场景。
修复方案
- 统一加锁顺序:所有涉及多表、多行写入的事务,严格按照相同的顺序申请锁资源,比如统一按表ID从小到大、行主键从小到大的顺序执行操作,从根源消除循环等待的可能。
- 拆分大事务:将执行时间长、操作多的大事务拆分为多个小事务,尽可能缩短锁的持有时间。
- 调整隔离级别:业务允许的前提下,开启READ COMMITTED SNAPSHOT或SNAPSHOT ISOLATION隔离级别,用行版本机制替代共享锁,大幅降低读写阻塞概率。
- 补充覆盖索引:给写入语句的WHERE条件、关联条件添加对应索引,缩小写入时的锁范围,避免不必要的大范围行锁甚至表锁。
- 限制并行执行:针对频繁触发死锁的写入语句,添加
OPTION (MAXDOP 1)hint强制关闭并行执行计划,避免单语句内部的通信缓冲区死锁。 - 添加死锁重试逻辑:在Function代码中捕获错误码1205的异常,按照指数退避策略重试执行事务,降低业务侧的失败率。
- 削峰填谷控制并发:通过队列中间件缓冲写入请求、限制Function的最大并发数等方式,平滑写入峰值,避免短时间内大量请求同时打向SQL数据库。
内容的提问来源于stack exchange,提问作者Akhilesh Jaiswal
相关产品推荐
相关产品推荐

