C#教程

关注公众号 jb51net

关闭
首页 > 软件编程 > C#教程 > C#接口幂等性实现方案

C#中实现接口幂等性的四种实战方案

作者:墨夶

在分布式系统和高并发场景中,接口的幂等性(Idempotency)是保障数据一致性的核心能力,本文将深入解析 C#中4种实现接口幂等性的实战方案,每种方案均附带 完整代码示例 和 场景分析,涵盖从数据库约束到分布式锁的全方位解决方案,需要的朋友可以参考下

为什么你的接口在高并发下重复执行?

在分布式系统和高并发场景中,接口的幂等性(Idempotency)是保障数据一致性的核心能力。想象一下:用户提交订单后网络延迟,前端重复点击“支付”,结果系统扣款两次;或者因重试机制触发了重复的转账请求。这些问题的根本原因在于接口缺乏幂等性设计。

本文将深入解析 C#中4种实现接口幂等性的实战方案,每种方案均附带 完整代码示例场景分析,涵盖从数据库约束到分布式锁的全方位解决方案。通过本文,你将掌握如何在实际项目中构建“永不重复”的接口逻辑。

方案一:基于唯一标识符的幂等性校验

核心原理

为每个请求分配一个全局唯一的标识符(如UUID或业务编号),服务端通过检查该标识符是否已处理过,决定是否执行操作。

适用场景

代码实现

/// <summary>
/// 数据库上下文(EF Core)
/// </summary>
public class ApplicationDbContext : DbContext
{
    public DbSet<RequestLog> RequestLogs { get; set; }

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }
}

/// <summary>
/// 请求日志实体
/// </summary>
public class RequestLog
{
    [Key] // 唯一主键约束
    public string RequestId { get; set; } = Guid.NewGuid().ToString();
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;
    public string BusinessType { get; set; } // 业务类型(如"Payment")
}

/// <summary>
/// 服务层逻辑
/// </summary>
public class PaymentService
{
    private readonly ApplicationDbContext _context;

    public PaymentService(ApplicationDbContext context)
    {
        _context = context;
    }

    /// <summary>
    /// 处理支付请求
    /// </summary>
    /// <param name="requestId">唯一请求ID</param>
    /// <returns></returns>
    public async Task<bool> ProcessPayment(string requestId)
    {
        try
        {
            // 1. 尝试插入请求记录(利用主键约束防重)
            var log = new RequestLog
            {
                RequestId = requestId,
                BusinessType = "Payment"
            };

            await _context.RequestLogs.AddAsync(log);
            await _context.SaveChangesAsync(); // 若RequestId已存在,会抛出DbUpdateException

            // 2. 执行核心业务逻辑(如扣款)
            await DeductBalanceAsync();

            return true;
        }
        catch (DbUpdateException ex)
        {
            // 3. 捕获主键冲突异常,判定为重复请求
            Console.WriteLine($"请求ID {requestId} 已处理过。异常:{ex.Message}");
            return false;
        }
    }

    private async Task DeductBalanceAsync()
    {
        // 模拟扣款逻辑
        await Task.Delay(100);
    }
}

注意事项

方案二:乐观锁版本控制

核心原理

通过版本号(Version)字段控制数据更新,仅当版本号匹配时允许操作。

适用场景

代码实现

/// <summary>
/// 库存实体
/// </summary>
public class ProductStock
{
    [Key]
    public int ProductId { get; set; }
    public int Stock { get; set; }
    public int Version { get; set; } // 乐观锁版本号
}

/// <summary>
/// 库存服务
/// </summary>
public class StockService
{
    private readonly ApplicationDbContext _context;

    public StockService(ApplicationDbContext context)
    {
        _context = context;
    }

    /// <summary>
    /// 扣减库存(乐观锁)
    /// </summary>
    /// <param name="productId">商品ID</param>
    /// <param name="quantity">扣减数量</param>
    /// <returns></returns>
    public async Task<bool> DeductStock(int productId, int quantity)
    {
        while (true)
        {
            try
            {
                // 1. 查询当前库存及版本号
                var product = await _context.ProductStocks
                    .FirstOrDefaultAsync(p => p.ProductId == productId);

                if (product == null || product.Stock < quantity)
                    return false;

                // 2. 执行扣减并更新版本号(原子操作)
                product.Stock -= quantity;
                product.Version += 1;

                await _context.SaveChangesAsync();
                return true;
            }
            catch (DbUpdateConcurrencyException ex)
            {
                // 3. 版本冲突时重试
                Console.WriteLine("检测到并发修改,重试中...");
                await Task.Delay(10); // 避免忙等待
            }
        }
    }
}

注意事项

方案三:基于Redis的Token机制

核心原理

在请求前获取一个唯一Token,服务端验证Token有效性并标记已使用,防止重复提交。

适用场景

代码实现

/// <summary>
/// Redis Token服务
/// </summary>
public class TokenService
{
    private readonly IDatabase _redisDb;

    public TokenService(IConnectionMultiplexer redis)
    {
        _redisDb = redis.GetDatabase();
    }

    /// <summary>
    /// 生成Token并缓存
    /// </summary>
    /// <param name="businessKey">业务标识(如用户ID+订单号)</param>
    /// <param name="expireMinutes">过期时间(分钟)</param>
    /// <returns></returns>
    public string GenerateToken(string businessKey, int expireMinutes = 5)
    {
        var token = Guid.NewGuid().ToString();
        var key = $"idempotent:token:{businessKey}";

        _redisDb.StringSet(key, token, TimeSpan.FromMinutes(expireMinutes));
        return token;
    }

    /// <summary>
    /// 验证并消耗Token
    /// </summary>
    /// <param name="businessKey"></param>
    /// <param name="token"></param>
    /// <returns></returns>
    public bool ValidateToken(string businessKey, string token)
    {
        var key = $"idempotent:token:{businessKey}";
        var storedToken = _redisDb.StringGet(key);

        if (storedToken.IsNullOrEmpty || !storedToken.ToString().Equals(token))
            return false;

        // 原子删除Token(防止并发问题)
        _redisDb.KeyDelete(key);
        return true;
    }
}

/// <summary>
/// 控制器示例
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
    private readonly TokenService _tokenService;
    private readonly ApplicationDbContext _context;

    public OrderController(TokenService tokenService, ApplicationDbContext context)
    {
        _tokenService = tokenService;
        _context = context;
    }

    [HttpPost("submit")]
    public async Task<IActionResult> SubmitOrder([FromBody] OrderRequest request)
    {
        var businessKey = $"{request.UserId}:{request.OrderNo}";
        var isValid = _tokenService.ValidateToken(businessKey, request.Token);

        if (!isValid)
            return BadRequest("重复提交或Token无效");

        // 执行核心逻辑
        await CreateOrderAsync(request);
        return Ok("订单提交成功");
    }
}

注意事项

方案四:分布式锁(Redis + RedLock)

核心原理

通过分布式锁(如Redis RedLock)强制请求串行化,确保同一操作在分布式环境中只执行一次。

适用场景

代码实现

/// <summary>
/// Redis分布式锁服务
/// </summary>
public class DistributedLockService
{
    private readonly IConnectionMultiplexer _redis;

    public DistributedLockService(IConnectionMultiplexer redis)
    {
        _redis = redis;
    }

    /// <summary>
    /// 尝试获取分布式锁
    /// </summary>
    /// <param name="lockKey">锁标识</param>
    /// <param name="lockValue">锁值(通常为请求ID)</param>
    /// <param name="expiry">过期时间</param>
    /// <returns></returns>
    public async Task<bool> TryAcquireLock(string lockKey, string lockValue, TimeSpan expiry)
    {
        var redisDb = _redis.GetDatabase();
        return await redisDb.StringSetAsync(lockKey, lockValue, expiry, When.NotExists);
    }

    /// <summary>
    /// 释放分布式锁
    /// </summary>
    public async Task ReleaseLock(string lockKey, string lockValue)
    {
        var script = @"
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
        ";

        var redisDb = _redis.GetDatabase();
        await redisDb.ScriptEvaluateAsync(script, new[] { lockKey }, new[] { lockValue });
    }
}

/// <summary>
/// 服务层示例
/// </summary>
public class TransferService
{
    private readonly DistributedLockService _lockService;
    private readonly ApplicationDbContext _context;

    public TransferService(DistributedLockService lockService, ApplicationDbContext context)
    {
        _lockService = lockService;
        _context = context;
    }

    /// <summary>
    /// 执行转账(分布式锁保护)
    /// </summary>
    public async Task<bool> ExecuteTransfer(string transferId, decimal amount)
    {
        var lockKey = $"transfer:{transferId}";
        var requestId = Guid.NewGuid().ToString();
        var expiry = TimeSpan.FromSeconds(30); // 锁超时时间

        try
        {
            // 1. 尝试获取锁
            if (!await _lockService.TryAcquireLock(lockKey, requestId, expiry))
                return false; // 已被其他线程处理

            // 2. 执行核心逻辑
            await TransferMoneyAsync(amount);
            return true;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"转账失败:{ex.Message}");
            return false;
        }
        finally
        {
            // 3. 释放锁
            await _lockService.ReleaseLock(lockKey, requestId);
        }
    }

    private async Task TransferMoneyAsync(decimal amount)
    {
        // 模拟转账逻辑
        await Task.Delay(200);
    }
}

注意事项

如何选择最适合的方案?

方案优点缺点适用场景
唯一标识符实现简单,数据库原生支持需维护额外表订单、支付等业务场景
乐观锁无锁竞争,性能高需处理重试逻辑库存扣减、状态更新
Token机制用户友好,跨服务兼容性强依赖Redis等中间件表单提交、支付确认
分布式锁强一致性,适用于复杂场景性能开销大,需处理死锁跨服务核心业务

** 幂等性不是银弹,但它是底线**

在分布式系统中,接口的幂等性设计是避免数据混乱的最后防线。通过本文的4种方案,你可以根据业务需求灵活选择:

以上就是C#中实现接口幂等性的四种实战方案的详细内容,更多关于C#接口幂等性实现方案的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文