支付交易 - 防止重复扣款
概述
支付系统是幂等性设计最重要的应用场景之一。重复扣款会导致严重的资损和用户投诉,因此必须通过严格的幂等性控制来保证资金安全。
核心挑战
- 网络超时:支付请求超时后重试可能导致重复扣款
- 用户误操作:用户多次点击支付按钮
- 第三方支付回调:微信/支付宝可能多次通知同一笔支付
- 分布式事务:扣款、更新订单、发送通知需要保持一致性
架构设计
客户端 API Gateway 支付服务 数据库
| | | |
|-- 发起支付 ------------->| | |
| (Idempotency-Key) | | |
| |-- 检查幂等键 ---------->| |
| | |-- 查询 Redis ----------|
| | |<-- 返回结果 -----------|
| | | |
| |<-- 已处理,直接返回 ------| |
|<-- 返回支付结果 -----------| | |
| | | |
|-- 重试(相同Key) --------->| | |
| |-- 检查幂等键 ---------->| |
| | |-- 查询 Redis ----------|
| | |<-- 返回缓存结果 -------|
| |<-- 返回缓存结果 ---------| |
|<-- 返回相同结果 -----------| | |完整实现
1. 数据库表设计(PostgreSQL)
sql
-- 支付记录表
CREATE TABLE payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id),
user_id UUID NOT NULL REFERENCES users(id),
-- 支付信息
amount DECIMAL(10, 2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
payment_method VARCHAR(20) NOT NULL, -- alipay, wechat, stripe
-- 第三方支付信息
third_party_payment_id VARCHAR(100), -- 支付宝/微信的交易号
third_party_response JSONB,
-- 状态
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, success, failed, refunded
error_message TEXT,
-- 幂等性控制
idempotency_key VARCHAR(64) UNIQUE NOT NULL,
idempotency_locked_at TIMESTAMP WITH TIME ZONE,
-- 时间戳
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
paid_at TIMESTAMP WITH TIME ZONE,
-- 乐观锁
version INTEGER NOT NULL DEFAULT 0
);
-- 索引
CREATE INDEX idx_payments_order_id ON payments(order_id);
CREATE INDEX idx_payments_user_id ON payments(user_id);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_idempotency_key ON payments(idempotency_key);
CREATE INDEX idx_payments_third_party_id ON payments(third_party_payment_id);
-- 支付流水表(记录每次尝试)
CREATE TABLE payment_attempts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
payment_id UUID NOT NULL REFERENCES payments(id),
-- 尝试信息
attempt_number INTEGER NOT NULL,
request_data JSONB,
response_data JSONB,
-- 结果
status VARCHAR(20) NOT NULL, -- success, failed, timeout
error_code VARCHAR(50),
error_message TEXT,
-- 时间
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_payment_attempts_payment_id ON payment_attempts(payment_id);2. C# 实体类
csharp
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public class Payment
{
[Key]
public Guid Id { get; set; }
[Required]
public Guid OrderId { get; set; }
public Order Order { get; set; }
[Required]
public Guid UserId { get; set; }
public User User { get; set; }
[Required]
[Column(TypeName = "decimal(10,2)")]
public decimal Amount { get; set; }
[Required]
[MaxLength(3)]
public string Currency { get; set; } = "CNY";
[Required]
[MaxLength(20)]
public string PaymentMethod { get; set; }
[MaxLength(100)]
public string? ThirdPartyPaymentId { get; set; }
public JsonDocument? ThirdPartyResponse { get; set; }
[Required]
[MaxLength(20)]
public string Status { get; set; } = "pending";
public string? ErrorMessage { get; set; }
[Required]
[MaxLength(64)]
public string IdempotencyKey { get; set; }
public DateTime? IdempotencyLockedAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime? PaidAt { get; set; }
[ConcurrencyCheck]
public int Version { get; set; }
// 导航属性
public ICollection<PaymentAttempt> Attempts { get; set; } = new List<PaymentAttempt>();
}
public class PaymentAttempt
{
[Key]
public Guid Id { get; set; }
[Required]
public Guid PaymentId { get; set; }
public Payment Payment { get; set; }
[Required]
public int AttemptNumber { get; set; }
public JsonDocument? RequestData { get; set; }
public JsonDocument? ResponseData { get; set; }
[Required]
[MaxLength(20)]
public string Status { get; set; }
[MaxLength(50)]
public string? ErrorCode { get; set; }
public string? ErrorMessage { get; set; }
public DateTime CreatedAt { get; set; }
}3. 支付服务实现
csharp
public class PaymentService
{
private readonly AppDbContext _dbContext;
private readonly IIdempotencyTokenService _idempotencyService;
private readonly IPaymentGateway _paymentGateway; // 第三方支付网关
private readonly ILogger<PaymentService> _logger;
public PaymentService(
AppDbContext dbContext,
IIdempotencyTokenService idempotencyService,
IPaymentGateway paymentGateway,
ILogger<PaymentService> logger)
{
_dbContext = dbContext;
_idempotencyService = idempotencyService;
_paymentGateway = paymentGateway;
_logger = logger;
}
/// <summary>
/// 创建支付(带幂等性保证)
/// </summary>
public async Task<Result<Payment>> CreatePaymentAsync(
Guid orderId,
decimal amount,
string paymentMethod,
string idempotencyKey)
{
// 验证幂等键
if (string.IsNullOrWhiteSpace(idempotencyKey))
{
return Result<Payment>.Failure("Idempotency key is required");
}
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// 检查是否已存在相同的幂等键
var existingPayment = await _dbContext.Payments
.FirstOrDefaultAsync(p => p.IdempotencyKey == idempotencyKey);
if (existingPayment != null)
{
_logger.LogInformation("Payment already exists for key: {Key}", idempotencyKey);
return Result<Payment>.Success(existingPayment);
}
// 验证订单
var order = await _dbContext.Orders.FindAsync(orderId);
if (order == null)
{
return Result<Payment>.Failure("Order not found");
}
if (order.Status != "pending")
{
return Result<Payment>.Failure("Order is not in pending status");
}
// 验证金额
if (amount != order.TotalAmount)
{
return Result<Payment>.Failure("Payment amount does not match order total");
}
// 创建支付记录
var payment = new Payment
{
Id = Guid.NewGuid(),
OrderId = orderId,
UserId = order.UserId,
Amount = amount,
Currency = "CNY",
PaymentMethod = paymentMethod,
Status = "pending",
IdempotencyKey = idempotencyKey,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Version = 0
};
_dbContext.Payments.Add(payment);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInformation("Payment created: {PaymentId}, Key: {Key}",
payment.Id, idempotencyKey);
return Result<Payment>.Success(payment);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Failed to create payment for order: {OrderId}", orderId);
return Result<Payment>.Failure("Failed to create payment");
}
}
/// <summary>
/// 处理支付(调用第三方支付网关)
/// </summary>
public async Task<Result<PaymentResult>> ProcessPaymentAsync(Guid paymentId)
{
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// 获取支付记录(带锁)
var payment = await _dbContext.Payments
.FirstOrDefaultAsync(p => p.Id == paymentId);
if (payment == null)
{
return Result<PaymentResult>.Failure("Payment not found");
}
// 检查状态
if (payment.Status == "success")
{
_logger.LogWarning("Payment already completed: {PaymentId}", paymentId);
return Result<PaymentResult>.Success(new PaymentResult
{
Success = true,
PaymentId = payment.Id,
ThirdPartyPaymentId = payment.ThirdPartyPaymentId
});
}
if (payment.Status != "pending")
{
return Result<PaymentResult>.Failure($"Payment is in {payment.Status} status");
}
// 更新状态为 processing
payment.Status = "processing";
payment.UpdatedAt = DateTime.UtcNow;
await _dbContext.SaveChangesAsync();
// 记录尝试
var attempt = new PaymentAttempt
{
Id = Guid.NewGuid(),
PaymentId = payment.Id,
AttemptNumber = payment.Attempts.Count + 1,
Status = "processing",
CreatedAt = DateTime.UtcNow
};
_dbContext.PaymentAttempts.Add(attempt);
await _dbContext.SaveChangesAsync();
// 调用第三方支付网关
PaymentGatewayResponse gatewayResponse;
try
{
gatewayResponse = await _paymentGateway.ChargeAsync(new PaymentChargeRequest
{
Amount = payment.Amount,
Currency = payment.Currency,
PaymentMethod = payment.PaymentMethod,
OrderId = payment.OrderId,
IdempotencyKey = payment.IdempotencyKey // 传递幂等键给第三方
});
}
catch (Exception ex)
{
// 网关调用失败
attempt.Status = "failed";
attempt.ErrorMessage = ex.Message;
await _dbContext.SaveChangesAsync();
throw; // 抛出异常让上层处理重试
}
// 更新尝试记录
attempt.Status = gatewayResponse.Success ? "success" : "failed";
attempt.ResponseData = gatewayResponse.RawResponse;
attempt.ErrorCode = gatewayResponse.ErrorCode;
attempt.ErrorMessage = gatewayResponse.ErrorMessage;
// 更新支付记录
if (gatewayResponse.Success)
{
payment.Status = "success";
payment.ThirdPartyPaymentId = gatewayResponse.TransactionId;
payment.ThirdPartyResponse = gatewayResponse.RawResponse;
payment.PaidAt = DateTime.UtcNow;
// 更新订单状态
var order = await _dbContext.Orders.FindAsync(payment.OrderId);
if (order != null)
{
order.Status = "paid";
order.PaidAt = DateTime.UtcNow;
}
_logger.LogInformation("Payment succeeded: {PaymentId}, Transaction: {TxnId}",
payment.Id, gatewayResponse.TransactionId);
}
else
{
payment.Status = "failed";
payment.ErrorMessage = gatewayResponse.ErrorMessage;
_logger.LogWarning("Payment failed: {PaymentId}, Error: {Error}",
payment.Id, gatewayResponse.ErrorMessage);
}
payment.UpdatedAt = DateTime.UtcNow;
payment.Version++;
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return Result<PaymentResult>.Success(new PaymentResult
{
Success = gatewayResponse.Success,
PaymentId = payment.Id,
ThirdPartyPaymentId = gatewayResponse.TransactionId,
ErrorMessage = gatewayResponse.ErrorMessage
});
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Failed to process payment: {PaymentId}", paymentId);
return Result<PaymentResult>.Failure("Payment processing failed");
}
}
/// <summary>
/// 处理第三方支付回调( webhook )
/// </summary>
public async Task HandlePaymentCallbackAsync(PaymentCallback callback)
{
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// 根据第三方交易 ID 查找支付记录
var payment = await _dbContext.Payments
.FirstOrDefaultAsync(p => p.ThirdPartyPaymentId == callback.TransactionId);
if (payment == null)
{
// 尝试根据订单 ID 查找
payment = await _dbContext.Payments
.FirstOrDefaultAsync(p => p.OrderId == callback.OrderId && p.Status == "pending");
if (payment == null)
{
_logger.LogWarning("Payment not found for callback: {TxnId}", callback.TransactionId);
return;
}
}
// 幂等性检查:如果已经处理过,直接返回
if (payment.Status == "success")
{
_logger.LogInformation("Payment callback already processed: {PaymentId}", payment.Id);
return;
}
// 使用乐观锁更新
payment.Status = callback.Success ? "success" : "failed";
payment.ThirdPartyResponse = callback.RawData;
payment.UpdatedAt = DateTime.UtcNow;
payment.Version++;
if (callback.Success)
{
payment.PaidAt = callback.PaidAt ?? DateTime.UtcNow;
// 更新订单状态
var order = await _dbContext.Orders.FindAsync(payment.OrderId);
if (order != null)
{
order.Status = "paid";
order.PaidAt = payment.PaidAt;
}
}
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
_logger.LogInformation("Payment callback processed: {PaymentId}, Status: {Status}",
payment.Id, payment.Status);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Failed to process payment callback: {TxnId}", callback.TransactionId);
throw;
}
}
}
public class PaymentResult
{
public bool Success { get; set; }
public Guid PaymentId { get; set; }
public string? ThirdPartyPaymentId { get; set; }
public string? ErrorMessage { get; set; }
}4. 控制器实现
csharp
[ApiController]
[Route("api/[controller]")]
public class PaymentsController : ControllerBase
{
private readonly PaymentService _paymentService;
public PaymentsController(PaymentService paymentService)
{
_paymentService = paymentService;
}
/// <summary>
/// 创建支付
/// </summary>
[HttpPost]
public async Task<ActionResult<Payment>> CreatePayment(
[FromBody] CreatePaymentRequest request,
[FromHeader(Name = "Idempotency-Key")] string idempotencyKey)
{
if (string.IsNullOrWhiteSpace(idempotencyKey))
{
return BadRequest(new { error = "Idempotency-Key header is required" });
}
var result = await _paymentService.CreatePaymentAsync(
request.OrderId,
request.Amount,
request.PaymentMethod,
idempotencyKey);
if (!result.IsSuccess)
{
return BadRequest(new { error = result.Error });
}
return CreatedAtAction(nameof(GetPayment), new { id = result.Data.Id }, result.Data);
}
/// <summary>
/// 处理支付
/// </summary>
[HttpPost("{id}/process")]
public async Task<ActionResult<PaymentResult>> ProcessPayment(Guid id)
{
var result = await _paymentService.ProcessPaymentAsync(id);
if (!result.IsSuccess)
{
return BadRequest(new { error = result.Error });
}
return Ok(result.Data);
}
/// <summary>
/// 查询支付状态
/// </summary>
[HttpGet("{id}")]
public async Task<ActionResult<Payment>> GetPayment(Guid id)
{
var payment = await _dbContext.Payments.FindAsync(id);
if (payment == null)
{
return NotFound();
}
return Ok(payment);
}
/// <summary>
/// 支付回调(webhook)
/// </summary>
[HttpPost("callback/{provider}")]
public async Task<IActionResult> PaymentCallback(string provider, [FromBody] JsonElement callback)
{
try
{
var paymentCallback = ParseCallback(provider, callback);
await _paymentService.HandlePaymentCallbackAsync(paymentCallback);
return Ok(new { status = "success" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process payment callback from {Provider}", provider);
return StatusCode(500, new { status = "error" });
}
}
}客户端最佳实践
csharp
public class PaymentClient
{
private readonly HttpClient _httpClient;
public async Task<Payment> CreatePaymentWithRetry(
CreatePaymentRequest request,
int maxRetries = 3)
{
// 生成幂等键(基于业务数据)
var idempotencyKey = GenerateIdempotencyKey(request);
for (int i = 0; i < maxRetries; i++)
{
try
{
var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/payments");
httpRequest.Headers.Add("Idempotency-Key", idempotencyKey);
httpRequest.Content = JsonContent.Create(request);
var response = await _httpClient.SendAsync(httpRequest);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Payment>();
}
catch (HttpRequestException ex) when (i < maxRetries - 1)
{
_logger.LogWarning(ex, "Payment attempt {Attempt} failed, retrying...", i + 1);
// 指数退避
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
}
}
throw new Exception("Payment failed after retries");
}
private string GenerateIdempotencyKey(CreatePaymentRequest request)
{
// 基于业务数据生成确定性幂等键
var data = $"{request.OrderId}_{request.Amount}_{DateTime.UtcNow:yyyyMMdd}";
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(data));
return Convert.ToHexString(hash)[..32].ToLowerInvariant();
}
}监控告警
csharp
public class PaymentMetrics
{
private readonly Counter<long> _paymentsTotal;
private readonly Counter<long> _paymentsSucceeded;
private readonly Counter<long> _paymentsFailed;
private readonly Counter<long> _duplicatePayments;
private readonly Histogram<double> _paymentProcessingTime;
public void RecordPayment(bool success, bool isDuplicate, double processingTimeMs)
{
_paymentsTotal.Add(1);
if (success)
{
_paymentsSucceeded.Add(1);
}
else
{
_paymentsFailed.Add(1);
}
if (isDuplicate)
{
_duplicatePayments.Add(1);
}
_paymentProcessingTime.Record(processingTimeMs);
}
}总结
支付系统的幂等性设计要点:
✅ 多层防护:幂等键 + 唯一索引 + 乐观锁
✅ 事务控制:确保数据一致性
✅ 详细日志:记录每次尝试和回调
✅ 监控告警:及时发现异常
✅ 客户端配合:生成确定性幂等键
⚠️ 注意事项:
- 幂等键要基于业务数据生成
- 第三方支付也要传递幂等键
- Webhook 回调要做幂等检查
- 定期清理过期数据