两个地域各有一台应用服务器,一个客户端通过 VPN 写入,还有一个没有人写下来的问题:哪一部分决定写入已经发生?路由把请求送到某个进程。事务边界决定这次修改何时变得持久且可见。这是不同系统承担的不同职责,而两者之间的空隙,正是大多数多地域写入缺陷的所在。

从业务规则出发,而不是从地图出发

先用一句话说清业务规则,而不是地理:哪些读和写必须一起成功,在此期间哪些并发修改必须不可能发生。对库存预占来说,这意味着库存检查和预占要么同时生效,要么都不生效,并且其间不得有第二个订单消耗同一个单位。

RequirementWhat satisfies itWhat does not
All-or-nothing applicationA transaction inside one owning systemA VPN path between regions
No double spendIsolation strong enough to exclude the conflictAtomicity alone
Survive a region lossReplication the database itself documentsTwo independent databases behind one client

团队跳过的恰恰是中间那一行。没有足够隔离的原子性,会让两个事务各自都正确提交,却仍然产生业务认为不可能的状态,而且事后无论多少重试逻辑都无法修补。

一个权威边界胜过两台地域服务器

地域应用服务器可以原地保留,只要它们把操作交给某个在单一受支持事务边界内执行它的服务。应用层变成意图的路由器;数据层拥有事实。

让这个边界保持简短,并靠近它所触达的数据。在人类回答提示、或调用跨两个地域期间一直开着的事务,会把时间花在持有锁上,并提高其身后其他操作超时的可能性。这种故障看起来像数据库问题,实际上却是伪装成数据库问题的设计问题。把决定下移到数据层,通常能把它彻底消除。

BEGIN;
UPDATE inventory SET reserved = reserved + 1
 WHERE sku = 'A-100' AND available >= 1;
INSERT INTO reservation (sku, order_id) VALUES ('A-100', 'ord-9f');
COMMIT;

把这段示意读作一个边界,而不是性能建议。要点在于检查与插入共享同一次提交,因此并发的订单要么看到库存已被消耗,要么根本看不到。

复制数据库给得了什么,给不了什么

当复制数据库自身的事务模型支持时,它可以提供更宽的边界。Spanner 记录了在单个数据库内跨表的原子读写事务,并定义了隔离级别与重试行为。这是一项你要阅读、测试并刻意依赖的保证。

这也是一项来自产品的保证,而不是来自路径。地域之间的 VPN 改变数据包如何传输、故障如何呈现。无论隧道看起来多健康,它都不会把事务边界扩大哪怕一行。把两者混为一谈,会产生每个组件都通过自己的检查、数据却仍然不一致的架构。

两个数据库不会变成一个系统

两个独立管理的数据库不会因为客户端能同时访问它们,就获得共享事务管理器。如果业务确实需要跨越这一对的原子承诺,协调机制、参与方和恢复流程都必须经过选择与验证,而不能假定。

想一想工作准备就绪之后参与方或链路消失的那一刻。受支持的分布式提交可能留下需要恢复流程和人工决策的不确定状态,而且它并不承诺每个地域在每个分区期间都保持可写。

如果改而接受异步协调,就明确说明,并写下中间业务状态与对账规则。带补偿的本地已提交步骤链是正当设计;它只是不是换了个名字的单一原子事务。

在第一次故障之前设计重试

客户端可能在操作提交之后丢失响应,因此调用方不能把超时当作失败。保留持久的请求标识,并在重试变更之前使用服务结果或幂等机制,否则重试就变成第二次写入。

在可重试的中止之后,自动事务回调也可能执行不止一次,这使它们不适合承载不可逆的外部效果。除非工作流为它们定义了明确的安全协议,否则应把这些放在回调之外。同样的谨慎也适用于任何花钱或发消息的操作。重复扣款远比重复日志行难解释。

验收应重放四个场景:两个客户端争抢同一行、服务声称可重试的中止、响应永远不回来的提交,以及某个地域或它们之间链路的丢失。把已提交的业务结果与未解决状态记录在路由设计旁边,以免有人把网络误当成提供保证的组件。

相关文章