Idempotency-Key头+服务端幂等校验最稳妥:客户端生成唯一key(如UUID v4),服务端用Redis原子SET NX判重并缓存结果,单机可用IMemoryCache但需设过期时间,资金类等关键操作须叠加业务状态校验。

用 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
真正难的不是加一层缓存,而是厘清“什么算重复”——这取决于业务语义,不是技术能自动推断的。










