Npgsql COMMIT PREPARED 报不存在:检查被吞掉的事务错误
Npgsql 10.0.3 的两阶段事务组合中,SQL 已失败却继续 Complete,可能在后续 COMMIT PREPARED 才记录不存在。保留首个异常传播,仅在全部成功后完成作用域,并核对各参与者结果。
PostgreSQL 日志出现 COMMIT PREPARED,随后报 prepared transaction with identifier ... does not exist,外层 TransactionScope 却没有及时表现为整组失败。若更早有 22012、25P02 等 SQL 错误被内部函数捕获后继续执行,应先检查是否仍无条件调用了 scope.Complete()。
Npgsql 10.0.3、PostgreSQL 18.6 的具体组合中,事务已经失败,PREPARE 阶段没有建立可供提交的准备事务,驱动却向协调者报告 Prepared,随后才在 COMMIT PREPARED 出错。这条触发组合还加入另一个参与者以进入两阶段通知流程;不能把所有单库事务或所有“准备事务不存在”日志都归到同一分支。
Complete 表达成功意愿,不是成功凭据
TransactionScope.Complete 的含义是告诉事务管理器,本作用域的操作已经成功,可以参与提交决策。它不保证全部资源最终提交,也不替业务检查此前被吞掉的数据库错误。
10.0.3 的 VolatileResourceManager.Prepare 执行 PREPARE TRANSACTION,只有命令执行抛出异常才调用 ForceRollback;没有在这段流程中另外检查失败事务状态。PostgreSQL 的失败事务可以走回滚结果,因而“这次调用没抛异常”并不能证明对应准备事务存在。后面的 Commit 则捕获并记录错误,再结束参与者通知。
这解释了为什么最晚的提交日志不一定是最早的失败。保留关联标识和脱敏后的时间线,向前查找同一事务里的第一个 SQL 异常;不要只重试最后那条 COMMIT PREPARED。
让失败回到拥有作用域的调用方
检查数据库访问封装是否捕获 PostgresException 后只写日志,返回默认值或继续报告成功。无法在当前事务中恢复的错误应继续抛出,或以明确失败结果传到拥有 TransactionScope 的代码,保证它不会再进入成功分支。
Complete 应当只位于全部必要操作成功之后,通常作为 using 块内的最后一步;不要放在 finally 中,也不要在失败结果上仍调用它。异步调用还应保留原有的作用域流转选项与生命周期,不能为改异常传播而把操作移出原事务。
确实需要局部失败后继续业务时,应先明确事务和保存点的设计,再按 Npgsql、PostgreSQL 与协调方式的实际支持范围实现。不能把捕获异常本身当成回滚到保存点,更不能在失败事务中反复发普通 SQL,期待它自动恢复。
这里不要求应用伪造 ForceRollback 回调,也不应靠反射修改驱动私有状态。暂时避免这条路径的重点是阻止已失败的操作被上报为成功;它不会修复驱动内部对 PREPARE 结果的判断。
核对各参与者结果,再决定是否重试业务
在隔离环境注入一次获准的数据库失败,确认失败能到达作用域所有者、Complete 不执行,并检查数据库及其他参与者都走预期回滚路径。再覆盖正常成功的组合,确认提交行为和资源释放不受影响。
遇到真实线上故障时,要核对每个参与者的实际结果和业务幂等条件,再按原有补偿流程处理。不能看到一方没有准备事务,就直接重跑可能已经影响其他资源的整组操作。后续升级正式驱动后,仍用“底层失败被传播”和“两阶段完成结果”分别回归,不能只以外层没有抛错判断提交成功。


