幂等性指多次执行同一请求对系统状态的改变效果等价于执行一次;C# Web API 必须考虑它,因 POST 等非幂等操作(如创建订单)易因重试或连点导致重复提交,仅前端防抖不足,需后端通过 Idempotency-Key、分布式缓存与数据库约束协同保障。

什么是幂等性,为什么 C# Web API 必须考虑它
幂等性不是“调用一次和调用十次结果一样”,而是“多次执行同一请求,对系统状态的改变效果等价于执行一次”。在 C# Web API 中,如果一个 POST /orders 接口没做幂等控制,用户连点两次提交按钮,就可能生成两条重复订单——这不是前端防抖能解决的,后端必须兜底。
HTTP 方法本身有语义约定:GET、PUT、DELETE 天然应是幂等的;但 POST 不是。而现实中大量业务操作(创建订单、发起支付、扣减库存)都用 POST,所以得自己实现幂等逻辑。
用 Idempotency-Key + 缓存实现最简可靠方案
主流做法是客户端在请求头带上唯一标识 Idempotency-Key,服务端用它作为键,缓存该请求的响应结果或执行状态。关键不在“怎么存”,而在“什么时候存、存什么、存多久”。
-
Idempotency-Key应由客户端生成(如 UUID v4),服务端只校验格式和长度,不生成 - 缓存建议用
IDistributedCache(如 Redis),避免单机内存缓存导致负载均衡下失效 - 缓存值推荐存
(status code, response body, timestamp)三元组,而非仅“已执行”,否则无法正确返回原始响应 - 过期时间要大于业务最大处理耗时(比如支付回调最长 10s,那就设 30s),但不宜过长(防止 key 泄露占用资源)
public class IdempotencyFilter : ActionFilterAttribute
{
private readonly IDistributedCache _cache;
public IdempotencyFilter(IDistributedCache cache) => _cache = cache;
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var key = context.HttpContext.Request.Headers["Idempotency-Key"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(key))
{
context.Result = new BadRequestObjectResult("Missing Idempotency-Key header");
return;
}
var cacheKey = $"idempotent:{key}";
var cached = await _cache.GetAsync(cacheKey, context.HttpContext.RequestAborted);
if (cached != null)
{
var result = JsonSerializer.Deserialize(cached);
context.Result = new ObjectResult(result.Body) { StatusCode = result.StatusCode };
return;
}
var exec = await next();
if (exec.Exception == null && context.Result is ObjectResult obj && obj.Value != null)
{
var response = new IdempotentResponse
{
StatusCode = obj.StatusCode ?? 200,
Body = obj.Value
};
await _cache.SetAsync(cacheKey, JsonSerializer.SerializeToUtf8Bytes(response),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30) });
}
} }
数据库层面的幂等保障不能只靠 UNIQUE 约束
光靠给订单表加 UNIQUE (user_id, client_order_id) 是不够的:约束触发时抛的是 SqlException,直接 500,且无法区分是“重复提交”还是“其他冲突”。必须把约束失败转化为可控的业务响应。
BJXSHOP网上开店专家
BJXShop网上购物系统是一个高效、稳定、安全的电子商店销售平台,经过近三年市场的考验,在中国网购系统中属领先水平;完善的订单管理、销售统计系统;网站模版可DIY、亦可导入导出;会员、商品种类和价格均实现无限等级;管理员权限可细分;整合了多种在线支付接口;强有力搜索引擎支持... 程序更新:此版本是伴江行官方商业版程序,已经终止销售,现于免费给大家使用。比其以前的免费版功能增加了:1,整合了论坛
下载
- 用
TRY...CATCH捕获 SQL Server 的错误号 2627(唯一键冲突)或 2601(主键重复) - PostgreSQL 对应捕获
23505(unique_violation) - EF Core 中更推荐用
ExecuteSqlRaw执行带ON CONFLICT DO NOTHING(PG)或MERGE(SQL Server)的语句,避免异常路径 - 注意:数据库幂等只保证“写入不重复”,不保证“响应一致”——仍需配合缓存返回原始成功响应
别忽略分布式锁和并发边界
当幂等校验 + 写入需要原子性(比如先查缓存、再写 DB、再存缓存),单纯用 IDistributedCache 的 GetAsync/SetAsync 无法防止竞态。两个相同请求几乎同时到达,都发现缓存无值,都会去执行业务逻辑。
这时候得加一层轻量级分布式锁:
- 用
Redis SET key value NX EX 10(NX=不存在才设,EX=10秒过期)抢锁 - 抢到锁的请求走完整流程,释放锁前把结果写入缓存;没抢到的请求等待后重查缓存
- 锁超时必须小于业务最大耗时,否则会误释放;也不宜太短(如 1s),否则频繁重试加重压力
真正难的不是代码怎么写,而是幂等键的设计粒度:是按用户+操作类型+时间戳?还是绑定具体业务实体 ID?一旦选错,要么锁住不该锁的请求,要么放行本该拦截的重复请求。









