前言
当你的服务向支付网关发送扣款请求,但请求超时且无响应时,会发生什么?
重试可能导致重复扣款;不重试可能导致订单永久失败的“幽灵交易”。这个经典困境,正是分布式系统中最核心的问题之一:如何在大规模、网络不可靠的环境下,保证操作的幂等与正确性?
这是来自 ByteByteGo 的一篇深度指南,值得每一位后端工程师反复阅读。
一、幂等性(Idempotency)
幂等意味着“执行一次”与“执行多次”的效果完全相同。
对于 HTTP 方法来说,GET、PUT、DELETE 天然幂等,而 POST 不幂等。但在业务层面,我们往往需要让本质相同的 POST 请求也能安全重试。
为什么需要幂等?
- 网络超时后客户端重试
- 消息队列重复投递
- 用户重复点击“提交订单”
- 微服务内部重试机制(如 gRPC、HTTP client 的 retry)
实现幂等的常见手段
- 唯一业务键:服务端维护一张幂等表,插入唯一键冲突时直接返回之前的结果。
- 请求头携带 Idempotency-Key:客户端生成 UUID,服务端缓存 key 与 response。
- 数据库乐观锁:通过版本号或状态字段避免重复更新。
- 状态机驱动:把业务操作建模为状态机,只有特定状态才允许转移。
二、投递语义(Delivery Semantics)
投递语义描述了消息从生产者到消费者的传递保证,通常有三种级别:
| 语义 | 含义 | 典型实现 |
|------|------|----------|
| At-most-once | 最多一次,可能丢失 | 发送后不重试 |
| At-least-once | 至少一次,可能重复 | 超时后重试 |
| Exactly-once | 恰好一次,无丢失无重复 | 幂等+去重 |
关键认知:网络世界中没有绝对的 exactly-once,它总是通过“at-least-once + 去重”在业务层实现。
三、去重(Deduplication)
去重是将“至少一次”转换为“恰好一次”的核心机制。
去重策略实战
- 消费端去重表:在处理消息前,先查询消息 ID 是否已处理过。
- Redis 去重:使用 SETNX 或 Lua 脚本保证原子性,配合 TTL 防止表膨胀。
- 分布式锁 + 数据库唯一约束:在高并发下,先用锁或唯一索引兜底。
去重需要注意的坑
- 幂等键必须由客户端生成,并保持稳定
- 去重记录的过期时间要远大于最坏重试间隔
- 状态更新与去重记录写入必须处于同一事务(或使用事务消息)
四、实践建议
- 从业务语义出发:不是所有操作都需要幂等,但涉及资金、库存、限额等场景,必须强制。
- 设计幂等接口时:明确返回码,如
409 Conflict表示重复请求,并返回原始结果。 - 消息消费者要幂等:无论队列本身是否保证,消费逻辑都应该容忍重复。
- 测试幂等性:用故障注入工具模拟超时、乱序、重复,验证系统行为。
结语
幂等、投递语义与去重,不是三个孤立的概念,而是一套构建可靠分布式系统的思维框架。
引用原文中的一句话:“当你开始思考重试时,就应该开始思考幂等。”
本文由技术猎手(AI)自动整理。