快速参考
📋 常用代码片段
1. ASP.NET Core 幂等性中间件注册
csharp
// Program.cs
builder.Services.AddIdempotency(options =>
{
options.HeaderName = "Idempotency-Key";
options.DefaultExpiry = TimeSpan.FromHours(24);
});
app.UseIdempotency();2. 生成幂等键
csharp
// 基于 GUID
var idempotencyKey = Guid.NewGuid().ToString();
// 基于业务数据
var idempotencyKey = $"{userId}_{orderId}_{DateTime.UtcNow:yyyyMMdd}";
// 基于哈希
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(data));
var idempotencyKey = Convert.ToHexString(hash)[..32];3. PostgreSQL 唯一索引
sql
-- 单字段唯一索引
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- 复合唯一索引
CREATE UNIQUE INDEX idx_orders_user_number
ON orders(user_id, order_number);
-- 部分唯一索引
CREATE UNIQUE INDEX idx_subscriptions_active
ON subscriptions(user_id, service_id)
WHERE status = 'active';4. Redis 原子操作
csharp
// SET NX EX - 原子设置
var acquired = await _redis.StringSetAsync(
key,
value,
TimeSpan.FromSeconds(30),
When.NotExists);
// Lua 脚本 - 原子删除
var luaScript = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";5. 处理唯一约束冲突
csharp
try
{
await _dbContext.SaveChangesAsync();
}
catch (DbUpdateException ex) when (IsUniqueViolation(ex))
{
// 处理重复
return Result.Failure("Record already exists");
}
private bool IsUniqueViolation(DbUpdateException ex)
{
return ex.InnerException is PostgresException pgEx
&& pgEx.SqlState == "23505";
}🔧 常用命令
PostgreSQL
sql
-- 查看当前锁等待
SELECT * FROM pg_locks WHERE NOT granted;
-- 查看索引使用情况
SELECT * FROM pg_stat_user_indexes;
-- 清理过期数据
DELETE FROM idempotency_tokens
WHERE expires_at < NOW() - INTERVAL '7 days';
-- 查看表大小
SELECT pg_size_pretty(pg_total_relation_size('payments'));Redis
bash
# 查看所有键
KEYS idempotency:*
# 查看键的 TTL
TTL idempotency:token:abc123
# 删除键
DEL idempotency:token:abc123
# 查看内存使用
INFO memory.NET CLI
bash
# 添加 NuGet 包
dotnet add package StackExchange.Redis
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
# 运行项目
dotnet run
# 运行测试
dotnet test📊 HTTP 方法速查
| 方法 | 安全 | 幂等 | 需要额外设计 |
|---|---|---|---|
| GET | ✅ | ✅ | ❌ |
| HEAD | ✅ | ✅ | ❌ |
| OPTIONS | ✅ | ✅ | ❌ |
| POST | ❌ | ❌ | ✅ 必须 |
| PUT | ❌ | ✅ | ❌ |
| DELETE | ❌ | ✅ | ❌ |
| PATCH | ❌ | ⚠️ | ⚠️ 可能需要 |
🎯 实现方案选择指南
场景 → 推荐方案
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 表单提交防重 | Token 机制 | 用户体验好 |
| 订单创建 | 唯一索引 + Token | 双重保障 |
| 支付扣款 | 唯一索引 + 乐观锁 | 强一致性 |
| 消息队列去重 | 全局唯一 ID | 简单高效 |
| 库存扣减 | 乐观锁 | 高性能 |
| 分布式系统 | Redis 分布式锁 | 跨节点同步 |
| 高并发场景 | Redis + 队列 | 削峰填谷 |
⚡ 性能优化技巧
1. 减少锁粒度
csharp
// ❌ 粗粒度:锁住整个方法
public async Task ProcessOrder(Guid userId)
{
lock (_lock)
{
// 所有用户的请求都串行
}
}
// ✅ 细粒度:按用户分区
public async Task ProcessOrder(Guid userId)
{
var userLock = _locks[userId.GetHashCode() % _lockCount];
await userLock.WaitAsync();
try
{
// 只有同一用户的请求串行
}
finally
{
userLock.Release();
}
}2. 批量操作
csharp
// ❌ 逐条插入
foreach (var item in items)
{
_dbContext.Items.Add(item);
await _dbContext.SaveChangesAsync();
}
// ✅ 批量插入
_dbContext.Items.AddRange(items);
await _dbContext.SaveChangesAsync();3. 异步处理
csharp
// ❌ 同步处理,阻塞响应
public IActionResult CreateOrder(OrderRequest request)
{
_orderService.Process(request); // 耗时 2s
return Ok();
}
// ✅ 异步队列,快速响应
public async Task<IActionResult> CreateOrder(OrderRequest request)
{
await _channel.Writer.WriteAsync(request);
return Accepted(new { requestId = Guid.NewGuid() });
}🐛 常见问题排查
问题 1:幂等性不生效
检查清单:
- [ ] 客户端是否正确传递了
Idempotency-Key - [ ] 服务端是否正确读取了请求头
- [ ] Redis/数据库连接是否正常
- [ ] 键的格式是否正确
调试代码:
csharp
_logger.LogInformation("Received Idempotency-Key: {Key}",
context.Request.Headers["Idempotency-Key"]);问题 2:性能下降
检查清单:
- [ ] 是否有数据库锁竞争
- [ ] Redis 响应时间是否正常
- [ ] 是否有大量超时请求
- [ ] 索引是否合理
诊断查询:
sql
-- 查看慢查询
SELECT query, mean_time, calls
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;问题 3:死锁
症状:
- 请求长时间无响应
- 数据库日志中出现 deadlock detected
解决方案:
- 确保事务尽可能短
- 避免嵌套锁
- 设置合理的超时时间
- 使用相同的顺序获取锁
📝 最佳实践速查
设计原则
✅ DO:
- 为所有 POST 请求设计幂等性
- 使用 UUID 作为幂等键
- 设置合理的过期时间
- 做好监控和告警
- 记录详细的日志
❌ DON'T:
- 不要忽略幂等性
- 不要使用可预测的幂等键
- 不要忘记清理过期数据
- 不要在锁内执行耗时操作
- 不要假设客户端一定会重试
安全建议
- HTTPS 传输:防止幂等键被窃听
- 一次性使用:Token 使用后立即失效
- 绑定用户:幂等键与用户会话关联
- 限流保护:防止恶意请求
- 审计日志:记录所有关键操作
🔍 监控指标
关键指标
csharp
// 1. 请求总量
metrics.Counter("idempotency.requests.total");
// 2. 缓存命中率
var hitRate = cacheHits / (cacheHits + cacheMisses);
// 3. 平均响应时间
metrics.Histogram("idempotency.response.time");
// 4. 错误率
var errorRate = errors / totalRequests;
// 5. 锁等待时间
metrics.Histogram("idempotency.lock.wait.time");告警规则
yaml
# Prometheus 告警规则示例
groups:
- name: idempotency
rules:
- alert: HighErrorRate
expr: rate(idempotency_errors_total[5m]) > 0.05
for: 5m
annotations:
summary: "High error rate on idempotency"
- alert: LowCacheHitRate
expr: idempotency_cache_hit_rate < 0.5
for: 10m
annotations:
summary: "Low cache hit rate"
- alert: HighLockWaitTime
expr: histogram_quantile(0.99, idempotency_lock_wait_time) > 1000
for: 5m
annotations:
summary: "High lock wait time"📚 扩展阅读
RFC 规范
官方文档
设计模式
🎓 面试常见问题
Q1: 什么是幂等性?
A: 幂等性是指同一个操作执行一次和执行多次产生的结果是一致的。在 API 设计中,意味着相同的请求无论发送多少次,服务器资源和返回结果都保持一致。
Q2: 如何实现 POST 请求的幂等性?
A:
- 客户端生成唯一的幂等键(Idempotency-Key)
- 服务端接收请求时检查该键
- 如果键已存在,返回缓存的结果
- 如果键不存在,处理请求并存储结果
Q3: Token 机制和唯一索引有什么区别?
A:
- Token 机制:应用层实现,灵活性高,支持复杂场景
- 唯一索引:数据库层实现,简单可靠,性能更好
通常两者结合使用,形成多层防护。
Q4: 如何处理高并发下的锁竞争?
A:
- 使用 Redis 代替数据库锁
- 实现分区策略,分散热点
- 使用异步队列削峰
- 优化索引,减少锁持有时间
💼 实战模板
订单创建模板
csharp
[HttpPost]
public async Task<ActionResult<Order>> CreateOrder(
[FromBody] CreateOrderRequest request,
[FromHeader(Name = "Idempotency-Key")] string idempotencyKey)
{
// 1. 验证幂等键
if (string.IsNullOrWhiteSpace(idempotencyKey))
{
return BadRequest("Idempotency-Key is required");
}
// 2. 检查是否已处理
var cached = await _cache.GetAsync<Order>($"order:{idempotencyKey}");
if (cached != null)
{
return Ok(cached);
}
// 3. 创建订单
var order = await _orderService.CreateOrderAsync(request, idempotencyKey);
// 4. 缓存结果
await _cache.SetAsync($"order:{idempotencyKey}", order, TimeSpan.FromHours(24));
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}支付处理模板
csharp
[HttpPost("{id}/pay")]
public async Task<IActionResult> PayOrder(
Guid id,
[FromBody] PaymentRequest request)
{
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// 1. 获取订单(带锁)
var order = await _dbContext.Orders
.FirstOrDefaultAsync(o => o.Id == id && o.Status == "pending");
if (order == null)
{
return NotFound("Order not found or already paid");
}
// 2. 调用支付网关
var paymentResult = await _paymentGateway.ChargeAsync(new PaymentChargeRequest
{
Amount = order.TotalAmount,
IdempotencyKey = $"order_{id}" // 传递幂等键
});
// 3. 更新订单状态
if (paymentResult.Success)
{
order.Status = "paid";
order.PaidAt = DateTime.UtcNow;
}
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return Ok(paymentResult);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Payment failed for order: {OrderId}", id);
return StatusCode(500, "Payment processing failed");
}
}祝开发顺利! 🚀