什么是幂等性,为什么 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;
<pre class='brush:php;toolbar:false;'>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<IdempotentResponse>(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,且无法区分是“重复提交”还是“其他冲突”。必须把约束失败转化为可控的业务响应。 用
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?一旦选错,要么锁住不该锁的请求,要么放行本该拦截的重复请求。
