C#中實(shí)現(xiàn)接口冪等性的四種實(shí)戰(zhàn)方案
為什么你的接口在高并發(fā)下重復(fù)執(zhí)行?
在分布式系統(tǒng)和高并發(fā)場景中,接口的冪等性(Idempotency)是保障數(shù)據(jù)一致性的核心能力。想象一下:用戶提交訂單后網(wǎng)絡(luò)延遲,前端重復(fù)點(diǎn)擊“支付”,結(jié)果系統(tǒng)扣款兩次;或者因重試機(jī)制觸發(fā)了重復(fù)的轉(zhuǎn)賬請求。這些問題的根本原因在于接口缺乏冪等性設(shè)計。
本文將深入解析 C#中4種實(shí)現(xiàn)接口冪等性的實(shí)戰(zhàn)方案,每種方案均附帶 完整代碼示例 和 場景分析,涵蓋從數(shù)據(jù)庫約束到分布式鎖的全方位解決方案。通過本文,你將掌握如何在實(shí)際項(xiàng)目中構(gòu)建“永不重復(fù)”的接口邏輯。
方案一:基于唯一標(biāo)識符的冪等性校驗(yàn)
核心原理
為每個請求分配一個全局唯一的標(biāo)識符(如UUID或業(yè)務(wù)編號),服務(wù)端通過檢查該標(biāo)識符是否已處理過,決定是否執(zhí)行操作。
適用場景
- 支付、訂單創(chuàng)建等需要嚴(yán)格防重的場景。
- 業(yè)務(wù)天然具備唯一鍵(如訂單號)。
代碼實(shí)現(xiàn)
/// <summary>
/// 數(shù)據(jù)庫上下文(EF Core)
/// </summary>
public class ApplicationDbContext : DbContext
{
public DbSet<RequestLog> RequestLogs { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
}
/// <summary>
/// 請求日志實(shí)體
/// </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; } // 業(yè)務(wù)類型(如"Payment")
}
/// <summary>
/// 服務(wù)層邏輯
/// </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. 執(zhí)行核心業(yè)務(wù)邏輯(如扣款)
await DeductBalanceAsync();
return true;
}
catch (DbUpdateException ex)
{
// 3. 捕獲主鍵沖突異常,判定為重復(fù)請求
Console.WriteLine($"請求ID {requestId} 已處理過。異常:{ex.Message}");
return false;
}
}
private async Task DeductBalanceAsync()
{
// 模擬扣款邏輯
await Task.Delay(100);
}
}
注意事項(xiàng)
- 唯一鍵設(shè)計:確保
RequestId字段在數(shù)據(jù)庫中設(shè)置唯一索引。 - 性能優(yōu)化:可定期清理過期的
RequestLog表數(shù)據(jù)。 - 異常處理:需捕獲所有可能的并發(fā)異常(如死鎖、超時)。
方案二:樂觀鎖版本控制
核心原理
通過版本號(Version)字段控制數(shù)據(jù)更新,僅當(dāng)版本號匹配時允許操作。
適用場景
- 更新操作(如庫存扣減、狀態(tài)修改)。
- 需要防止并發(fā)覆蓋的業(yè)務(wù)(如秒殺系統(tǒng))。
代碼實(shí)現(xiàn)
/// <summary>
/// 庫存實(shí)體
/// </summary>
public class ProductStock
{
[Key]
public int ProductId { get; set; }
public int Stock { get; set; }
public int Version { get; set; } // 樂觀鎖版本號
}
/// <summary>
/// 庫存服務(wù)
/// </summary>
public class StockService
{
private readonly ApplicationDbContext _context;
public StockService(ApplicationDbContext context)
{
_context = context;
}
/// <summary>
/// 扣減庫存(樂觀鎖)
/// </summary>
/// <param name="productId">商品ID</param>
/// <param name="quantity">扣減數(shù)量</param>
/// <returns></returns>
public async Task<bool> DeductStock(int productId, int quantity)
{
while (true)
{
try
{
// 1. 查詢當(dāng)前庫存及版本號
var product = await _context.ProductStocks
.FirstOrDefaultAsync(p => p.ProductId == productId);
if (product == null || product.Stock < quantity)
return false;
// 2. 執(zhí)行扣減并更新版本號(原子操作)
product.Stock -= quantity;
product.Version += 1;
await _context.SaveChangesAsync();
return true;
}
catch (DbUpdateConcurrencyException ex)
{
// 3. 版本沖突時重試
Console.WriteLine("檢測到并發(fā)修改,重試中...");
await Task.Delay(10); // 避免忙等待
}
}
}
}
注意事項(xiàng)
- 重試機(jī)制:需設(shè)置最大重試次數(shù),避免無限循環(huán)。
- 事務(wù)隔離:確保查詢和更新操作在同一個事務(wù)中。
- 性能權(quán)衡:高并發(fā)下需評估重試成本。
方案三:基于Redis的Token機(jī)制
核心原理
在請求前獲取一個唯一Token,服務(wù)端驗(yàn)證Token有效性并標(biāo)記已使用,防止重復(fù)提交。
適用場景
- 表單提交、支付確認(rèn)等用戶交互場景。
- 需要跨服務(wù)共享防重邏輯。
代碼實(shí)現(xiàn)
/// <summary>
/// Redis Token服務(wù)
/// </summary>
public class TokenService
{
private readonly IDatabase _redisDb;
public TokenService(IConnectionMultiplexer redis)
{
_redisDb = redis.GetDatabase();
}
/// <summary>
/// 生成Token并緩存
/// </summary>
/// <param name="businessKey">業(yè)務(wù)標(biāo)識(如用戶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>
/// 驗(yàn)證并消耗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(防止并發(fā)問題)
_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("重復(fù)提交或Token無效");
// 執(zhí)行核心邏輯
await CreateOrderAsync(request);
return Ok("訂單提交成功");
}
}
注意事項(xiàng)
- Redis原子操作:使用
SET NX和DEL確保操作原子性。 - Token時效性:合理設(shè)置過期時間(如5分鐘),避免資源浪費(fèi)。
- 跨服務(wù)一致性:Token需通過接口傳遞或嵌入Cookie中。
方案四:分布式鎖(Redis + RedLock)
核心原理
通過分布式鎖(如Redis RedLock)強(qiáng)制請求串行化,確保同一操作在分布式環(huán)境中只執(zhí)行一次。
適用場景
- 跨服務(wù)調(diào)用的防重(如微服務(wù)架構(gòu))。
- 對數(shù)據(jù)一致性要求極高的核心業(yè)務(wù)。
代碼實(shí)現(xiàn)
/// <summary>
/// Redis分布式鎖服務(wù)
/// </summary>
public class DistributedLockService
{
private readonly IConnectionMultiplexer _redis;
public DistributedLockService(IConnectionMultiplexer redis)
{
_redis = redis;
}
/// <summary>
/// 嘗試獲取分布式鎖
/// </summary>
/// <param name="lockKey">鎖標(biāo)識</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>
/// 服務(wù)層示例
/// </summary>
public class TransferService
{
private readonly DistributedLockService _lockService;
private readonly ApplicationDbContext _context;
public TransferService(DistributedLockService lockService, ApplicationDbContext context)
{
_lockService = lockService;
_context = context;
}
/// <summary>
/// 執(zhí)行轉(zhuǎn)賬(分布式鎖保護(hù))
/// </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. 執(zhí)行核心邏輯
await TransferMoneyAsync(amount);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"轉(zhuǎn)賬失?。簕ex.Message}");
return false;
}
finally
{
// 3. 釋放鎖
await _lockService.ReleaseLock(lockKey, requestId);
}
}
private async Task TransferMoneyAsync(decimal amount)
{
// 模擬轉(zhuǎn)賬邏輯
await Task.Delay(200);
}
}
注意事項(xiàng)
- 鎖超時時間:需根據(jù)業(yè)務(wù)耗時合理設(shè)置,避免死鎖。
- RedLock算法:在分布式環(huán)境中建議使用RedLock算法提升可靠性。
- 性能影響:鎖競爭可能導(dǎo)致吞吐量下降,需結(jié)合業(yè)務(wù)優(yōu)先級使用。
如何選擇最適合的方案?
| 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場景 |
|---|---|---|---|
| 唯一標(biāo)識符 | 實(shí)現(xiàn)簡單,數(shù)據(jù)庫原生支持 | 需維護(hù)額外表 | 訂單、支付等業(yè)務(wù)場景 |
| 樂觀鎖 | 無鎖競爭,性能高 | 需處理重試邏輯 | 庫存扣減、狀態(tài)更新 |
| Token機(jī)制 | 用戶友好,跨服務(wù)兼容性強(qiáng) | 依賴Redis等中間件 | 表單提交、支付確認(rèn) |
| 分布式鎖 | 強(qiáng)一致性,適用于復(fù)雜場景 | 性能開銷大,需處理死鎖 | 跨服務(wù)核心業(yè)務(wù) |
** 冪等性不是銀彈,但它是底線**
在分布式系統(tǒng)中,接口的冪等性設(shè)計是避免數(shù)據(jù)混亂的最后防線。通過本文的4種方案,你可以根據(jù)業(yè)務(wù)需求靈活選擇:
- 輕量級場景:優(yōu)先使用唯一標(biāo)識符或樂觀鎖。
- 高并發(fā)場景:結(jié)合Token機(jī)制和Redis緩存。
- 核心業(yè)務(wù):用分布式鎖保障強(qiáng)一致性。
以上就是C#中實(shí)現(xiàn)接口冪等性的四種實(shí)戰(zhàn)方案的詳細(xì)內(nèi)容,更多關(guān)于C#接口冪等性實(shí)現(xiàn)方案的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Unity通過UGUI的Slider調(diào)整物體顏色
這篇文章主要為大家詳細(xì)介紹了Unity通過UGUI的Slider調(diào)整物體顏色,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-02-02
利用C#實(shí)現(xiàn)分布式數(shù)據(jù)庫查詢
利用C#實(shí)現(xiàn)分布式數(shù)據(jù)庫查詢...2007-03-03
Unity5.6大規(guī)模地形資源創(chuàng)建方法
這篇文章主要為大家詳細(xì)介紹了Unity5.6大規(guī)模地形資源創(chuàng)建方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-02-02
詳解C#中多態(tài)性學(xué)習(xí)/虛方法/抽象方法和接口的用法
這篇文章主要為大家詳細(xì)介紹了C#中多態(tài)性學(xué)習(xí)、虛方法、抽象方法和接口的用法的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-03-03
C#中使用CAS實(shí)現(xiàn)無鎖算法的示例詳解
CAS(Compare-and-Swap)是一種多線程并發(fā)編程中常用的原子操作,用于實(shí)現(xiàn)多線程間的同步和互斥訪問。本文將利用CAS實(shí)現(xiàn)無鎖算法,需要的可以參考一下2023-04-04

