前言

当你的服务向支付网关发送扣款请求,但请求超时且无响应时,会发生什么?

重试可能导致重复扣款;不重试可能导致订单永久失败的“幽灵交易”。这个经典困境,正是分布式系统中最核心的问题之一:如何在大规模、网络不可靠的环境下,保证操作的幂等与正确性?

这是来自 ByteByteGo 的一篇深度指南,值得每一位后端工程师反复阅读。

一、幂等性(Idempotency)

幂等意味着“执行一次”与“执行多次”的效果完全相同。

对于 HTTP 方法来说,GET、PUT、DELETE 天然幂等,而 POST 不幂等。但在业务层面,我们往往需要让本质相同的 POST 请求也能安全重试。

为什么需要幂等?

  • 网络超时后客户端重试
  • 消息队列重复投递
  • 用户重复点击“提交订单”
  • 微服务内部重试机制(如 gRPC、HTTP client 的 retry)

实现幂等的常见手段

  1. 唯一业务键:服务端维护一张幂等表,插入唯一键冲突时直接返回之前的结果。
  2. 请求头携带 Idempotency-Key:客户端生成 UUID,服务端缓存 key 与 response。
  3. 数据库乐观锁:通过版本号或状态字段避免重复更新。
  4. 状态机驱动:把业务操作建模为状态机,只有特定状态才允许转移。

二、投递语义(Delivery Semantics)

投递语义描述了消息从生产者到消费者的传递保证,通常有三种级别:

| 语义 | 含义 | 典型实现 |

|------|------|----------|

| At-most-once | 最多一次,可能丢失 | 发送后不重试 |

| At-least-once | 至少一次,可能重复 | 超时后重试 |

| Exactly-once | 恰好一次,无丢失无重复 | 幂等+去重 |

关键认知:网络世界中没有绝对的 exactly-once,它总是通过“at-least-once + 去重”在业务层实现。

三、去重(Deduplication)

去重是将“至少一次”转换为“恰好一次”的核心机制。

去重策略实战

  • 消费端去重表:在处理消息前,先查询消息 ID 是否已处理过。
  • Redis 去重:使用 SETNX 或 Lua 脚本保证原子性,配合 TTL 防止表膨胀。
  • 分布式锁 + 数据库唯一约束:在高并发下,先用锁或唯一索引兜底。

去重需要注意的坑

  • 幂等键必须由客户端生成,并保持稳定
  • 去重记录的过期时间要远大于最坏重试间隔
  • 状态更新与去重记录写入必须处于同一事务(或使用事务消息)

四、实践建议

  1. 从业务语义出发:不是所有操作都需要幂等,但涉及资金、库存、限额等场景,必须强制。
  2. 设计幂等接口时:明确返回码,如 409 Conflict 表示重复请求,并返回原始结果。
  3. 消息消费者要幂等:无论队列本身是否保证,消费逻辑都应该容忍重复。
  4. 测试幂等性:用故障注入工具模拟超时、乱序、重复,验证系统行为。

结语

幂等、投递语义与去重,不是三个孤立的概念,而是一套构建可靠分布式系统的思维框架。

引用原文中的一句话:“当你开始思考重试时,就应该开始思考幂等。”

本文由技术猎手(AI)自动整理。