
从数学函数到状态机:幂等的底层机制
数学上,幂等函数满足 f(f(x)) = f(x)。比如绝对值函数 abs(abs(x)) = abs(x)。GET请求天然幂等,因为它只是查询,不改变状态。麻烦的是POST、PUT这类写操作。实现幂等,本质上要让写操作具备“可重复提交但效果唯一”的特性。 业界常见的做法有三种: 唯一约束法。给每次请求分配一个幂等键(Idempotency Key),服务端存储这个键和请求结果的映射。处理流程:先检查键是否存在,存在则返回已有结果;不存在则执行业务,存储结果,最后返回。这依赖数据库的唯一索引来保证并发安全。比如在MySQL建表:`CREATE UNIQUE INDEX idx_idem_key ON idem_records(key)`。但这里有个细节:必须先插入幂等记录(状态为“处理中”),再执行业务,最后更新状态为“成功”或“失败”。否则先业务后插入,并发时可能重复执行。
一次压测:MySQL与Redis的幂等性能对决

落地三大陷阱,个个要命
陷阱一:时钟是不靠谱的。很多人的幂等设计假设请求按时间顺序到达。但分布式环境下,网络延迟、时钟偏移会导致先发出的请求后到。如果你依赖时间戳来判定“哪个请求先到”,死定了。我们吃过亏:A请求先生成,但在网络上堵了一会儿;B请求后生成但先到达。结果B被处理,A到来时发现Key已存在,误以为重复,把B的结果返回给A,数据全错。解决方案:不要依赖绝对时间,要么使用严格递增的序列号(如数据库自增ID),要么用向量时钟或因果一致性协议。但最简单的,幂等键由客户端生成(UUID),服务端只负责“存在即重复”,不区分先后。 陷阱二:并发下的竞态条件。两个相同请求几乎同时到达,同时检查到幂等键不存在,都去执行业务。即便有唯一索引,插入时也会出现一个成功一个失败,但失败的那个可能返回“重复”,而实际上第一个请求的业务可能还未完成。用户看到的是“请求重复”,但实际上第一个请求还在跑,后续查询却发现没记录。解决办法:“先插入后执行业务”的变种:插入一条状态为“processing”的记录,成功插入的线程执行业务,完成后更新状态;插入失败的线程检查状态,若为“processing”则轮询等待或返回“处理中”,若为“success”则返回结果。这个方案需要控制轮询超时和死锁。 陷阱三:补偿操作需要幂等。在微服务的长事务Saga中,补偿操作本身也得幂等。比如取消订单的补偿,如果因为重试被执行两次,第一遍将库存加回,第二遍再加就超了。我们踩的坑:某个补偿接口直接调用了增量更新 `UPDATE inventory SET quantity=quantity+?`,没有记录补偿状态。重复补偿导致库存虚增。正确做法:补偿操作也要带上原事务的幂等键,并判断该补偿是否已执行过。简单加个状态表:`insert into compensation_log(saga_id, step, status) values(…)`,唯一索引,执行更新前先插入,冲突则跳过。 啰嗦这么多,其实幂等不难,难在承认网络和系统是不完美的。别信任何“这不可能重试”的担保。设计时多问一句:如果这个操作被调用了两次,系统会怎么样?回答不上来,就别上线。