什么是幂等

幂等(Idempotent):同一个操作执行多次与执行一次的效果相同。

数学类比:f(f(x)) = f(x)。

HTTP 方法的幂等性:

  • GET:幂等(读取不改变状态);
  • PUT:幂等(相同的内容设置多次,结果一样);
  • DELETE:幂等(删除多次,结果都是"已删除");
  • POST:不幂等(每次 POST 可能创建一个新资源)。

为什么重要

在分布式系统中,失败是常态:

  • 网络超时(可能是请求没到,也可能是响应没回来);
  • 服务暂时不可用;
  • 消息队列的重复投递(前文提到的"至少一次"语义)。

客户端通常会重试。如果操作不幂等,重试就可能造成重复扣款、重复下单等问题。

因此:凡是涉及"写"的接口,都应该考虑幂等性设计。

实现方法

一、唯一标识(幂等键)

客户端生成一个唯一请求 ID(如 UUID),随请求一起发送。

服务端:

  1. 收到请求,检查这个 ID 是否已处理过;
  2. 如果处理过,直接返回之前的结果;
  3. 如果没处理过,执行业务逻辑,并记录 ID 与结果。

存储:通常用 Redis(设置合理的过期时间)或数据库(唯一索引)。

注意:记录 ID 和业务处理必须在同一个事务中,否则可能出现"处理成功但没记录 ID"(重试导致重复)或"记录了 ID 但处理失败"(重试被误拦截)的情况。

二、数据库的唯一约束

利用数据库的唯一索引防止重复。

比如:订单表中对"用户 ID + 活动 ID"建立唯一索引,保证同一用户在同一活动中只能下一个单。

优点:数据库层面保证,可靠。
局限:只适用于能表达为唯一性约束的场景。

三、状态机

业务对象有明确的状态流转,重复的操作在错误的状态下会被拒绝。

比如:支付订单的状态是"待支付 → 已支付 → 已完成"。重复支付时,发现状态已经是"已支付",直接返回成功。

这是最自然的幂等设计——因为状态机本身就是业务的一部分。

四、乐观锁(版本号)

更新时带上版本号:

UPDATE account SET balance = balance - 100, version = version + 1
WHERE id = ? AND version = ?

如果版本号不匹配,说明数据已被修改,更新失败(可以重试或返回冲突)。

几个注意点

一、幂等键的过期时间。

如果永久保存,存储会无限增长。通常设置与业务周期匹配的过期时间(如 24 小时或 7 天)。

二、并发请求的处理。

如果同一个幂等键的两个请求同时到达,需要防止两者都通过检查(都要执行业务)。

解决:把"检查 + 记录"做成原子操作(如 Redis 的 SET NX,或数据库的唯一索引插入)。

三、返回结果要一致。

重试返回的应该是第一次执行的结果,而不是"该请求已处理"之类的不同响应。

否则客户端可能无法正确处理。

四、不是所有操作都适合幂等化。

比如"发送短信"这种有副作用的操作,幂等设计会让重试时不再发送——这可能是期望的(避免重复骚扰),也可能不是。

需要根据业务语义决定。

一句话

幂等性是分布式系统的基本功。有了它,重试才是安全的——而重试是应对网络不可靠的主要手段。

说明:本文为分布式系统设计的知识整理。