AI 执行治理之六:唯一约束护的是账,不是动作 一个助手调工具去写数据。这次调用可能**到达两次**模型自己重试一次、同一次确认被裁决两次、或者进程在中途重启。第二次到达不能真的再写一遍。常见的答案是给这次写一个**幂等键**再在账上给这个键加**唯一约束**。这个做法**单独用是不够的**。因为约束是在**写账**的时候生效的而写账发生在**动作之后**。所以账上确实只有一行而目标系统已经被写了两次。**唯一约束护的是账不是动作。**后面讲我们怎么补上这一块以及补完之后还剩一条我们**挡不住**的边界。## 一、为什么「查一下再做」不够先看最容易写的那个版本。执行前先查这个键**有没有**没有就执行执行完把键记下来。单线程下它没问题。并发下它是坏的**两份请求都读到了「还没有」**于是两边都执行。事后再记键的时候唯一约束把第二份挡掉了——**可动作已经发生过了**。所以约束挡下的是那次**登记**不是那次**写**。这就是上面那句话的意思**账上的约束管不到账外已经发生的事。**一句话概括这个形状**先查后做check-then-act** —— 判断和执行之间有一个窗口窗口里发生的事判断看不到。## 二、补法执行前先占位让占位本身当仲裁补法很小**把「查一下」换成「占一下」。**执行**之前**先往占位表里插一行。那一行的唯一约束就是这次的仲裁点- **插进去了** ⇒ 这次执行归我继续。- **插不进去**键已存在⇒ **说明已经有人持着这次执行****绝不重复执行**。关键不在「插一行」而在**插入发生在动作之前**。插入是原子的所以「谁拥有这次执行」在动作开始前就已定下**中间没有窗口**。这也解释了为什么占位表不能靠「写完成」来推如果我们只能事后知道「写完成了」那并发的那两份请求在动作开始前都是「还没有」——**又是一个 check-then-act**。## 三、那键该由什么构成这点值得单独说因为它决定了**什么算「同一笔写」**。我们的键是四样拼在一起取哈希哈希( 谁 : 哪一场对话 : 哪个工具 : 参数键名排序后 )逐样说- **谁** —— 同一个工具、同一组参数换个用户就是另一笔写。- **哪一场对话** —— 同一场对话里重复调用才叫重试跨对话的两次相同调用是两件事。- **哪个工具** —— 不必解释。- **参数且键名先排序** —— 这一步是为了让**同一次调用的两种写法算出同一个键**。不排序的话{a:1,b:2} 与 {b:2,a:1} 会算出两个键于是同一次调用被当成两笔写——**幂等就漏了**。这四样合起来回答的是「**这是不是同一次调用**」而**不是**「这次写会碰到什么」。这两个问题不一样把它们压成一个字段会出别的问题那是另一个题目这里只说键。## 四、占住之后失败了键要不要放掉这是最难的一处而我们的答案不是「放」或「不放」是**分两支**——依据是「**我们究竟知道什么**」。**本地写**我们知道结果。失败就是失败没落就是没落。这一支可以**放掉**占位让重试能真的重来也可以留着并如实标成失败。关键是——**我们知道自己知道**。**外部写**我们**不知道**。请求发出去了、对方没回话、连接断了——这三种情况里对方可能**收到了**。这时候如果放掉占位、让重试再来一次那第二次就是**一次真的双写**。所以外部那一支**故意把键占住**并在结果里写明 该请求**可能已到达**目标系统结果以目标系统为准 —— **本次不重试**。**这一句不是措辞是这一次的行为。** 「不知道」的时候不放行重试是因为**重试会把「不知道」变成「可能两次」**。## 五、重复到达时应该返回什么第二份请求不该再写一次但它也不该被当成失败——**它没做错什么**。我们的做法是**回放**。命中已有记录时把**当初那次的结果**返回给这次调用并标明这是复用{ id: 当初那条记录的 id, idempotent: true }调用方拿到的和第一次拿到的**是同一个结果**。对它来说「我调了一次、拿到了结果」这句话是真的。复合写还有一处必须处理一次调用写多行时**基键由主体那条占用**所以命中之后要**回放整组**而不是只回主体那一行——只回一行调用方会丢掉其余成员而那些成员**再也不会被声明**。## 六、一条我们挡不住的边界上面这套挡的是**我们自己**重发。它**不保证**端到端只发生一次。要做到那个**目标系统得认这个键**我们把键放进请求头一起发出去对方要不要据此去重是对方的事。所以准确的表述是**我们能保证自己不重复发不能保证对方只收一次。**这一条我们会写在结果消息里而不是留给读者去猜。## 七、一个你可以自己做的检验问四个问题任何一个答不好就是有窗口1. **判断和执行之间有没有窗口** 如果代码是「先查、再做」那窗口就在那儿。2. **「同一笔写」是由什么定义的** 如果键里没有「谁」或没有「哪一场对话」那么换个人、换个会话同一组参数会被当成同一笔——**那是另一种坏**。3. **键的参数部分排序了吗** 没排序同一次调用的两种写法会算出两个键。4. **失败之后重试会发生什么** 如果答案在「我们知道不知道对方收到了」之外那它多半是猜的。## 结语回到开头那句**唯一约束护的是账不是动作。** 这句话只要记住第 1 条检验就自己会做——**去问那个判断发生在动作之前还是之后**。我们做的是**执行前占位、占位即仲裁**键由「谁 / 哪一场对话 / 哪个工具 / 排序后的参数」构成**失败政策分两支**本地放掉、外部占住重复到达时**回放当初的结果**。最后照实交代两处。一是上面那条边界**我们不保证对方只收一次**。二是——**这套东西只在「参数能完整表达这次写」的时候成立**如果某个工具的行为还取决于参数之外的东西时间、随机、外部的当前状态那同一组参数两次调用本来就**不是同一笔写**键也就说不清。那种工具**得先让它把那些东西变成参数**否则键是假的。