订单系统 - 防止重复下单
概述
订单系统是幂等性设计的典型应用场景。用户可能因为网络延迟、页面刷新或误操作而多次提交订单,必须保证同一笔订单只创建一次。
核心挑战
- 用户重复点击:提交按钮被多次点击
- 网络超时重试:客户端自动重试
- 页面刷新:浏览器重新发送 POST 请求
- 并发请求:多个标签页同时提交
完整实现方案
1. 数据库设计(PostgreSQL)
sql
-- 订单表
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
-- 订单信息
order_number VARCHAR(50) NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
-- 幂等性控制
idempotency_key VARCHAR(64) UNIQUE NOT NULL, -- 唯一约束
-- 乐观锁
version INTEGER NOT NULL DEFAULT 0,
-- 时间戳
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
paid_at TIMESTAMP WITH TIME ZONE,
-- 软删除
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
);
-- 索引
CREATE UNIQUE INDEX idx_orders_idempotency_key ON orders(idempotency_key);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
-- 订单项表
CREATE TABLE order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL,
product_name VARCHAR(100) NOT NULL,
quantity INTEGER NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
subtotal DECIMAL(10, 2) NOT NULL
);
CREATE INDEX idx_order_items_order_id ON order_items(order_id);2. C# 实体类
csharp
public class Order
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public User User { get; set; }
public string OrderNumber { get; set; }
public decimal TotalAmount { get; set; }
public string Status { get; set; } = "pending";
// 幂等键
public string IdempotencyKey { get; set; }
// 乐观锁
[ConcurrencyCheck]
public int Version { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime? PaidAt { get; set; }
public bool IsDeleted { get; set; }
// 导航属性
public ICollection<OrderItem> Items { get; set; } = new List<OrderItem>();
}
public class OrderItem
{
public Guid Id { get; set; }
public Guid OrderId { get; set; }
public Order Order { get; set; }
public Guid ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Subtotal { get; set; }
}3. 订单服务实现
csharp
public class OrderService
{
private readonly AppDbContext _dbContext;
private readonly IDistributedLock _lock;
private readonly ILogger<OrderService> _logger;
public async Task<Result<Order>> CreateOrderAsync(
Guid userId,
CreateOrderRequest request,
string idempotencyKey)
{
// 验证幂等键
if (string.IsNullOrWhiteSpace(idempotencyKey))
{
return Result<Order>.Failure("Idempotency key is required");
}
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// 1. 检查是否已存在相同的幂等键
var existingOrder = await _dbContext.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.IdempotencyKey == idempotencyKey);
if (existingOrder != null)
{
_logger.LogInformation("Order already exists for key: {Key}", idempotencyKey);
return Result<Order>.Success(existingOrder);
}
// 2. 验证库存
foreach (var item in request.Items)
{
var product = await _dbContext.Products.FindAsync(item.ProductId);
if (product == null)
{
return Result<Order>.Failure($"Product {item.ProductId} not found");
}
if (product.Stock < item.Quantity)
{
return Result<Order>.Failure($"Insufficient stock for {product.Name}");
}
}
// 3. 生成订单号
var orderNumber = GenerateOrderNumber();
// 4. 创建订单
var order = new Order
{
Id = Guid.NewGuid(),
UserId = userId,
OrderNumber = orderNumber,
TotalAmount = request.Items.Sum(i => i.Quantity * i.UnitPrice),
Status = "pending",
IdempotencyKey = idempotencyKey,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Version = 0
};
// 5. 添加订单项
foreach (var item in request.Items)
{
order.Items.Add(new OrderItem
{
Id = Guid.NewGuid(),
ProductId = item.ProductId,
ProductName = item.ProductName,
Quantity = item.Quantity,
UnitPrice = item.UnitPrice,
Subtotal = item.Quantity * item.UnitPrice
});
}
_dbContext.Orders.Add(order);
await _dbContext.SaveChangesAsync();
// 6. 扣减库存(带乐观锁)
foreach (var item in request.Items)
{
var rowsAffected = await _dbContext.Database.ExecuteSqlRawAsync(
"UPDATE products SET stock = stock - @quantity, version = version + 1 WHERE id = @productId AND stock >= @quantity",
new NpgsqlParameter("@quantity", item.Quantity),
new NpgsqlParameter("@productId", item.ProductId));
if (rowsAffected == 0)
{
throw new InvalidOperationException($"Failed to deduct stock for product {item.ProductId}");
}
}
await transaction.CommitAsync();
_logger.LogInformation("Order created: {OrderId}, Number: {OrderNumber}",
order.Id, orderNumber);
return Result<Order>.Success(order);
}
catch (DbUpdateException ex) when (IsUniqueViolation(ex))
{
await transaction.RollbackAsync();
// 并发插入,返回已有订单
var existingOrder = await _dbContext.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.IdempotencyKey == idempotencyKey);
if (existingOrder != null)
{
return Result<Order>.Success(existingOrder);
}
return Result<Order>.Failure("Failed to create order due to concurrent modification");
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Failed to create order for user {UserId}", userId);
return Result<Order>.Failure($"Order creation failed: {ex.Message}");
}
}
private string GenerateOrderNumber()
{
// 格式:ORD-YYYYMMDD-HHMMSS-XXXXX
var now = DateTime.UtcNow;
var random = new Random().Next(10000, 99999);
return $"ORD-{now:yyyyMMdd}-{now:HHmmss}-{random}";
}
private bool IsUniqueViolation(DbUpdateException ex)
{
return ex.InnerException is PostgresException pgEx &&
pgEx.SqlState == "23505";
}
}4. 控制器实现
csharp
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class OrdersController : ControllerBase
{
private readonly OrderService _orderService;
[HttpPost]
[ProducesResponseType(typeof(Order), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<Order>> CreateOrder(
[FromBody] CreateOrderRequest request,
[FromHeader(Name = "Idempotency-Key")] string idempotencyKey)
{
var userId = User.GetUserId();
var result = await _orderService.CreateOrderAsync(userId, request, idempotencyKey);
if (!result.IsSuccess)
{
return BadRequest(new { error = result.Error });
}
return CreatedAtAction(
nameof(GetOrder),
new { id = result.Data.Id },
result.Data);
}
[HttpGet("{id}")]
public async Task<ActionResult<Order>> GetOrder(Guid id)
{
var order = await _dbContext.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null)
{
return NotFound();
}
return Ok(order);
}
}前端防重方案
React 实现
tsx
import { useState } from 'react';
function OrderForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [cartItems, setCartItems] = useState<CartItem[]>([]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting) {
console.warn('Already submitting, ignoring...');
return;
}
setIsSubmitting(true);
// 生成确定性幂等键
const idempotencyKey = generateIdempotencyKey(cartItems);
try {
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`,
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({ items: cartItems })
});
if (response.status === 409) {
alert('订单已提交,请勿重复提交');
return;
}
if (response.ok) {
const order = await response.json();
window.location.href = `/orders/${order.id}/success`;
}
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
{/* 购物车商品列表 */}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : '提交订单'}
</button>
</form>
);
}
// 基于购物车内容生成确定性幂等键
function generateIdempotencyKey(items: CartItem[]): string {
const data = items
.map(item => `${item.productId}:${item.quantity}`)
.sort()
.join('|');
const hash = crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
.substring(0, 32);
}监控与告警
1. 指标收集
csharp
public class OrderMetrics
{
private readonly Counter<long> _ordersCreated;
private readonly Counter<long> _duplicateOrders;
private readonly Histogram<double> _orderCreationTime;
public void RecordOrderCreated(bool isDuplicate, double creationTimeMs)
{
_ordersCreated.Add(1);
if (isDuplicate)
{
_duplicateOrders.Add(1);
}
_orderCreationTime.Record(creationTimeMs);
}
}2. Prometheus 告警
yaml
groups:
- name: order_alerts
rules:
- alert: HighDuplicateOrderRate
expr: rate(order_duplicates_total[5m]) / rate(order_created_total[5m]) > 0.05
for: 5m
annotations:
summary: "High duplicate order rate"
description: "More than 5% of orders are duplicates"
- alert: SlowOrderCreation
expr: histogram_quantile(0.99, order_creation_time_seconds) > 5
for: 5m
annotations:
summary: "Slow order creation"
description: "99th percentile order creation time exceeds 5 seconds"最佳实践总结
✅ DO
- 强制要求幂等键:所有创建订单请求必须携带
- 使用唯一索引:数据库层面保证不重复
- 前端防重:按钮禁用 + 防抖
- 详细日志:记录每次订单创建尝试
- 监控重复率:及时发现异常
❌ DON'T
- 不要仅依赖前端防护:后端必须有幂等性保证
- 不要忽略并发冲突:正确处理唯一约束违反
- 不要长时间持有事务:尽快提交释放锁
- 不要暴露内部错误:返回友好的错误信息
总结
订单系统的幂等性设计需要多层防护:
- 前端层:按钮禁用、防抖、确定性幂等键
- 传输层:Idempotency-Key 请求头
- 应用层:检查已有订单、分布式锁
- 数据层:唯一索引、事务控制
通过组合使用这些技术,可以有效防止重复下单,保证数据一致性。