用 Idempotency-Key
头 + 服务端幂等校验最稳妥
高并发下用户快速双击、网络重试、前端防抖失效,都会导致重复请求。光靠前端限制不可靠,必须后端兜底。最通用的做法是让客户端每次请求带上唯一
Idempotency-Key(比如 UUID v4),服务端用该 key 做原子性判重和结果缓存。
关键点:这个 key 必须由客户端生成(避免服务端生成时并发冲突),且在重试时复用同一 key。
Idempotency-Key值建议长度 ≥ 32 字符,避免哈希碰撞 服务端需用分布式锁或原子写(如 Redis 的
SET key value EX 3600 NX)确保“检查+写入”不被并发打断 缓存结果建议保留 1–24 小时,覆盖可能的延迟重试窗口 若业务要求强一致性(如扣款),需额外校验业务状态(如订单是否已支付),不能只依赖 key 缓存
ASP.NET Core 中用 IMemoryCache
做轻量幂等控制(仅限单节点)
如果 API 部署在单台服务器且 QPS 不超高(IMemoryCache 快速落地。注意它不跨进程,集群部署必须换 Redis。
public class IdempotentHandler
{
private readonly IMemoryCache _cache;
public IdempotentHandler(IMemoryCache cache) => _cache = cache;
public bool TryRegister(string idempotencyKey, out string cachedResult)
{
if (_cache.TryGetValue(idempotencyKey, out cachedResult))
return false; // 已存在,拒绝执行
_cache.Set(idempotencyKey, "", new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
Priority = CacheItemPriority.High
});
return true; // 可执行
}
}
配合中间件或 ActionFilter 使用,拦截
Idempotency-Key头,调用
TryRegister;返回
409 Conflict或直接返回缓存结果。 不要把完整响应体塞进
IMemoryCache,只存状态码和简要结果(如
{"orderId":"xxx"})
务必设置 AbsoluteExpirationRelativeToNow,避免内存泄漏 缓存 key 建议加前缀,如
$"idemp_{idempotencyKey}",防止和其他业务 key 冲突
Redis 实现分布式幂等(推荐生产环境)
集群部署必须用 Redis。核心逻辑是:用
SET key value EX 3600 NX原子写入,成功则执行业务,失败则查
GET key返回历史结果。
注意 Redis 的
NX是严格原子的,比先
EXISTS再
SET安全得多。 value 推荐存 JSON 字符串,包含
status(Success/Failed)、
response(原始响应体)、
timestamp若业务执行中崩溃,key 会自然过期,下次请求可重试 —— 这正是幂等设计的预期行为 避免用 Lua 脚本做复杂判断,简单场景用原生命令更可靠 连接 Redis 失败时,应降级为拒绝请求(
503 Service Unavailable),而不是跳过幂等校验
哪些操作不适合纯幂等 key 控制?
不是所有接口都适合只靠
Idempotency-Key拦截。以下情况必须叠加业务层校验: 涉及资金变动(如
POST /api/payments):需查数据库确认订单当前状态,防止“已退款又重复支付” 资源创建类接口(如
POST /api/orders):即使 key 重复,也要检查订单号是否已存在,避免因 key 泄露导致恶意构造 带时间敏感参数的请求(如含
expireAt):key 相同但参数不同,应视为不同请求,不能直接返回旧结果 幂等 key 被暴力枚举风险高时(如 key 来自用户可控字段),需增加签名或绑定用户 ID
真正难的不是加一层缓存,而是厘清“什么算重复”——这取决于业务语义,不是技术能自动推断的。
