什么是幂等
幂等(Idempotent):同一个操作执行多次与执行一次的效果相同。
数学类比:f(f(x)) = f(x)。
HTTP 方法的幂等性:
- GET:幂等(读取不改变状态);
- PUT:幂等(相同的内容设置多次,结果一样);
- DELETE:幂等(删除多次,结果都是"已删除");
- POST:不幂等(每次 POST 可能创建一个新资源)。
为什么重要
在分布式系统中,失败是常态:
- 网络超时(可能是请求没到,也可能是响应没回来);
- 服务暂时不可用;
- 消息队列的重复投递(前文提到的"至少一次"语义)。
客户端通常会重试。如果操作不幂等,重试就可能造成重复扣款、重复下单等问题。
因此:凡是涉及"写"的接口,都应该考虑幂等性设计。
实现方法
一、唯一标识(幂等键)
客户端生成一个唯一请求 ID(如 UUID),随请求一起发送。
服务端:
- 收到请求,检查这个 ID 是否已处理过;
- 如果处理过,直接返回之前的结果;
- 如果没处理过,执行业务逻辑,并记录 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,或数据库的唯一索引插入)。
三、返回结果要一致。
重试返回的应该是第一次执行的结果,而不是"该请求已处理"之类的不同响应。
否则客户端可能无法正确处理。
四、不是所有操作都适合幂等化。
比如"发送短信"这种有副作用的操作,幂等设计会让重试时不再发送——这可能是期望的(避免重复骚扰),也可能不是。
需要根据业务语义决定。
一句话
幂等性是分布式系统的基本功。有了它,重试才是安全的——而重试是应对网络不可靠的主要手段。
说明:本文为分布式系统设计的知识整理。