用幂等 key 解决网络超时重试导致的重复操作,数据库层面通过唯一约束防止并发请求同时写入两条记录,并讨论了不同业务场景的边界处理。
在构建 API 时,开发者通常会花大量时间思考认证、验证、性能和数据库设计。但还有一个概念,能够悄无声息地阻止一些严重的生产问题:幂等性(idempotency)。当 API 执行的动作只能发生一次时——比如创建支付、提交订单、注册用户或预订——幂等性就变得尤为重要。如果没有幂等性保护,一次简单的网络问题有时会把用户的单个操作变成多个操作。
简单来说,当多次发送相同的请求与只发送一次请求产生相同的预期结果时,这个 API 操作就是幂等的。假设用户点击了“立即支付”,请求到达服务器,支付已处理完毕,但响应因网络问题而延迟。用户的浏览器不知道支付是否成功,用户再次点击了按钮。此时服务器收到了同一操作的两次请求。没有保护机制,应用可能会处理两笔支付。有了幂等设计,服务器可以识别出第二次请求代表的是同一操作,从而返回原始请求的结果,而不是再次处理。这就是基本思路。
网络并非绝对可靠。请求可能因以下原因被重试:
重要的一点是,超时并不一定意味着操作失败了。服务器可能已经成功完成了操作,只是客户端没有收到响应。这就引出了一个重要问题:当客户端再次发送相同的请求时,应该发生什么?这正是幂等性变得有价值的地方。
以订单 API 为例:POST /api/orders
客户端发送:
{ "product_id": 123, "quantity": 1 }
如果请求超时且客户端重试,服务器可能会创建:
尽管用户只想下一个订单。一个常见的解决方案是提供幂等性密钥(idempotency key):
Idempotency-Key: 8f4a7c21-...
客户端为操作生成一个唯一密钥,随请求一起发送。服务器将该密钥与结果一起存储。如果相同的密钥再次到达,应用就知道该操作已经被处理过了。它不会创建另一个订单,而是返回原始结果。
一个重要的细节是,幂等性密钥应该代表一个特定的操作,而不仅仅是某个用户。例如:
payment-abc123如果该用户进行了另一笔合法支付,应该使用不同的密钥。否则,应用可能会错误地将新操作当作重复请求。更清晰的心智模型是:
一个业务操作 → 一个幂等性密钥
实现取决于应用,但数据库通常是实用的选择。一个简化的表可能包含:
idempotency_key
request_hash
status
response_code
response_body
created_at
当请求到达时,应用可以:
如果相同的密钥再次到达,返回存储的结果。具体的实现需要考虑并发、事务、过期和故障场景。
简单的实现中存在一个微妙的问题。假设两个相同的请求几乎同时到达:
请求 A → 检查密钥 → 未找到
请求 B → 检查密钥 → 未找到
两个请求都可能继续处理,这就违背了幂等性的目的。这就是为什么幂等性密钥通常需要数据库的唯一性约束或其他原子机制。例如:
UNIQUE(idempotency_key)
数据库可以帮助保证两个请求不能为同一密钥成功创建单独的记录。这是一个很好的例子,说明可靠的 API 设计不仅仅是编写应用代码。数据库约束可以是应用正确性模型的一部分。
另一个重要的设计决策是决定存储什么。假设一个请求创建了订单但之后发生了某些失败。
幂等性密钥是否应该可以重复使用?
没有标准答案。行为应该根据业务操作来定义。例如,支付 API 可能需要保留已完成支付尝试的结果,而另一种类型的操作可能允许客户端在验证失败或临时服务器故障后重试。重要的是要清晰定义生命周期,而不是将每种失败都当作简单的重试。
很容易产生这样的想法:“我只要检查这个订单是否已存在就行了。”但重复检测和幂等性并不一定是同一回事。应用可能合法地收到两条相似数据的订单。例如:
并不自动意味着第二条订单是重复的。幂等性密钥给了系统一个识别同一预期操作的方法。这个区别在支付、预订、结账和集成系统中尤为重要。
当操作产生现实世界的副作用时,幂等性变得特别有价值。例如:
在与第三方 API 集成时也很有用,因为重试是不可避免的。例如,如果你的应用向外部服务发送预订请求但没有收到响应,盲目地再次发送请求可能会产生重复预订。集成需要一个安全重试操作的策略。
我遵循的一个有用原则是:
如果重复 API 请求可能产生不良副作用,在将端点投入生产之前要考虑幂等性。
并非每个端点都需要幂等性密钥。像这样的读取操作:
GET /api/products/123
通常不需要,因为重复请求不会创建另一个产品。但像这样的操作:
POST /api/payments
可能需要更仔细的处理,因为重复执行可能产生另一笔金融交易。
幂等性是一个相对较小的概念,但它解决的是一个在生产规模下会变得大得多的问题。应用运行在不可靠的网络、分布式服务、队列、浏览器、移动设备、支付提供商和第三方 API 之上。重试是不可避免的。目标不是防止重试,而是让重试变得安全。因此,好的 API 设计不仅仅是让第一次请求工作,还包括决定当相同的请求再次到达时会发生什么。这就是为什么幂等性应该在任何可能产生重复操作的 API 设计中占有一席之地。