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);
}
}
注意事项
- 唯一键设计:确保
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
/// </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原子操作:使用
SET NX和DEL确保操作原子性。 - 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 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);
}
}
注意事项
- 锁超时时间:需根据业务耗时合理设置,避免死锁。
- RedLock算法:在分布式环境中建议使用RedLock算法提升可靠性。
- 性能影响:锁竞争可能导致吞吐量下降,需结合业务优先级使用。
如何选择最适合的方案?
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 唯一标识符 | 实现简单,数据库原生支持 | 需维护额外表 | 订单、支付等业务场景 |
| 乐观锁 | 无锁竞争,性能高 | 需处理重试逻辑 | 库存扣减、状态更新 |
| Token机制 | 用户友好,跨服务兼容性强 | 依赖Redis等中间件 | 表单提交、支付确认 |
| 分布式锁 | 强一致性,适用于复杂场景 | 性能开销大,需处理死锁 | 跨服务核心业务 |
** 幂等性不是银弹,但它是底线**
在分布式系统中,接口的幂等性设计是避免数据混乱的最后防线。通过本文的4种方案,你可以根据业务需求灵活选择:
- 轻量级场景:优先使用唯一标识符或乐观锁。
- 高并发场景:结合Token机制和Redis缓存。
- 核心业务:用分布式锁保障强一致性。
以上就是C#中实现接口幂等性的四种实战方案的详细内容,更多关于C#接口幂等性实现方案的资料请关注脚本之家其它相关文章!
