第 11 / 22 篇

第 10 章:提交响应丢失,不能直接当作回滚

客户端发送 COMMIT 后,数据库可能已经完成提交,而确认响应在网络中丢失。应用捕获到连接异常,却不能仅凭异常认定订单不存在。这是许多重复扣款、重复发货问题的起点。

10.1 把服务器事实与客户端知识分开

数据库已提交是一项服务器事实,客户端已收到成功确认是另一项事实。两者之间存在网络窗口。超时或连接断开,只能说明客户端没有取得可靠结果。

此时应进入 unknown 状态,使用稳定业务标识核查,而不是把同一个业务意图换个新键再次执行。若原连接仍有不确定状态,也不应继续让它承担后续业务。

10.2 一个真正切断响应的实验

随书故障代理只在环回地址监听,转发 MySQL 数据包。当它观察到客户端的 COMMIT 和服务器返回的 OK 后,关闭客户端连接,故意不转发确认。

测试从独立连接查询操作键,确认订单实际存在。由此同时得到两份证据:客户端收到错误,服务器记录已经提交。它比在程序里手工抛一个异常,更准确地制造了这个边界。

代理只用于明文教学连接,不解密 TLS、不记录数据包内容,也不适合作为生产代理。普通连接保留驱动自身的 TLS 行为,只有这个明确的环回故障注入连接禁用 TLS。实验开发过程曾因新驱动默认协商 TLS 而卡在握手,修复针对的是注入环境,不是取消生产传输保护。

10.3 查询状态也需要选择可信位置

如果提交后立即去一个有复制延迟的只读副本查询,暂时查不到不代表主库没有提交。业务需要明确查询位置、读一致性和等待策略。本书使用同一个单节点 MySQL 的独立连接核查,没有证明复制与故障切换语义。

若查询得到业务键,继续检查其参数、状态和结果;若暂时查不到,应按已定义的恢复协议处理,而不是由一个瞬时的空结果推导永不提交。长时间无法确认时,需要告警和授权核查流程。

10.4 不确定状态需要被保存

应用任务表可以保存请求身份、操作键、最后已知阶段、尝试记录和核查结果。它不能神奇地消除分布式不确定性,却能防止每个重试者都从零猜测。

补偿也需要依据。如果订单已经提交却没有返回响应,盲目“补偿扣款”可能反而撤销了有效订单;如果外部支付状态未知,数据库中的一条 rollback 也无法证明支付被撤销。

练习。 COMMIT 响应丢失后,应用在从库查不到订单,于是换一个操作键重试。请标出至少两个缺少证据的推断,并设计一条保留 unknown、查询原键和限制新执行的恢复路径。