c# 幂等性在c#接口设计中的实现

来源:这里教程网 时间:2026-02-21 17:37:16 作者:

什么是幂等性,为什么 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?一旦选错,要么锁住不该锁的请求,要么放行本该拦截的重复请求。

相关推荐