开发者

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

目录
  • 为什么你的接口在高并发下重复执行?
  • 方案一:基于唯一标识符的幂等性校验
    • 核心原理
    • 适用场景
    • 代码实现
    • 注意事项
  • 方案二:乐观锁版本控制
    • 核心原理
    • 适用场景
    • 代码实现
    • 注意事项
  • 方案三:基于Redis的Token机制
    • 核心原理
    • 适用场景
    • 代码实现
    • 注意事项
  • 方案四:分布式锁(Redis + RedLock)
    • 核心原理
    • 适用场景
    • 代码实现
    • 注意事项
  • 如何选择最适合的方案?
    • ** 幂等性不是银弹,但它是底线**

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

      在分布式系统和高并发场景中,接口的幂等性(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
      
                  // android2. 执行核心业务逻辑(如扣款)
                  await DeductBalanceAsync();
      
                  return true;
              }
              catch (DbUpdateException ex)
              {
                  // 3. 捕获主键冲突异常,判定为重复请求
                  Console.WriteLine($"请求ID {requestId} 已处理过。异常:{ex.Message}");
                  return false;
              }
          }
      
          private async Task DeductBalanceAsync()
          {
              // 模拟扣款逻辑
              await Task.Delay(100);
          }
      }
      

      注意事项

      • 唯一键设计:确保 RequestId 字段在数据库中设置唯一索引。
      • 性能优化:可定期清理过期的 RequestLog 表数据。
      • 异常处理:需捕获所有可能的并发异常(如死锁、超时)。

      方案二:乐观锁版本控制

      核心原理

      通过版本号(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
          /// <python;/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);
       php       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原子操作:使用 SET NXDEL 确保操作原子性。
      • Token时效性:合理设置过期时间(如5分钟),避免资源浪费。
      • 跨服务一致性:Token需通过接口传递或嵌入Cookie中。

      方案四:分布式锁(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 awaRHZxqit redisDb.StringSetAsync(lockKey, lockValue, expiry, When.NotExists);
          }
      
          /// <summary>
          /// 释放分布式锁
          /// </summary>
          public async Task ReleaseLock(string locphpkKey, 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);
          }
      }
      

      注意事项

      • 锁超时时间:需根据业务耗时合理设置,避免死锁。
      • RedLock算法:在分布式环境中建议使用RedLock算法提升可靠性。
      • 性能影响:锁竞争可能导致吞吐量下降,需结合业务优先级使用。

      如何选择最适合的方案?

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

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

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

      • 轻量级场景:优先使用唯一标识符或乐观锁。
      • 高并发场景:结合Token机制和Redis缓存。
      • 核心业务:用分布式锁保障强一致性。

      以上就是C#中实现接口幂等性的四种实战方案的详细内容,更多关于C#接口幂等性实现方案的资料请关注编程客栈(www.devze.com)其它相关文章!

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜