Skip to content

订单系统 - 防止重复下单 ​

概述 ​

订单系统是幂等性设计的典型应用场景。用户可能因为网络延迟、页面刷新或误操作而多次提交订单,必须保证同一笔订单只创建一次。

核心挑战 ​

  1. 用户重复点击:提交按钮被多次点击
  2. 网络超时重试:客户端自动重试
  3. 页面刷新:浏览器重新发送 POST 请求
  4. 并发请求:多个标签页同时提交

完整实现方案 ​

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 ​

  1. 强制要求幂等键:所有创建订单请求必须携带
  2. 使用唯一索引:数据库层面保证不重复
  3. 前端防重:按钮禁用 + 防抖
  4. 详细日志:记录每次订单创建尝试
  5. 监控重复率:及时发现异常

❌ DON'T ​

  1. 不要仅依赖前端防护:后端必须有幂等性保证
  2. 不要忽略并发冲突:正确处理唯一约束违反
  3. 不要长时间持有事务:尽快提交释放锁
  4. 不要暴露内部错误:返回友好的错误信息

总结 ​

订单系统的幂等性设计需要多层防护:

  1. 前端层:按钮禁用、防抖、确定性幂等键
  2. 传输层:Idempotency-Key 请求头
  3. 应用层:检查已有订单、分布式锁
  4. 数据层:唯一索引、事务控制

通过组合使用这些技术,可以有效防止重复下单,保证数据一致性。

Released under the MIT License.