一、什么是幂等性?为什么需要它?

想象一下你去ATM机取钱,第一次点击"确认"按钮时网络卡顿了,于是你又点了第二次。这时候你肯定不希望因为重复操作被扣两次钱,对吧?这就是幂等性要解决的问题——无论操作执行多少次,结果都应该和只执行一次一样。

在WCF服务中,客户端可能因为网络超时、服务响应慢等原因重复发送请求。如果服务端没有做好幂等设计,就可能出现重复扣款、重复下单等严重问题。我们来看个反面教材:

// 技术栈:C# + WCF
[ServiceContract]
public interface IOrderService
{
    // 非幂等的危险方法!
    [OperationContract]
    void PlaceOrder(int productId, int quantity);
}

public class OrderService : IOrderService
{
    public void PlaceOrder(int productId, int quantity)
    {
        // 直接扣减库存
        Inventory.Reduce(productId, quantity); 
        // 创建订单记录
        Orders.CreateNew(productId, quantity);
    }
}

这个服务如果被重复调用,库存就会被多次扣减。正确的做法应该像这样:

[ServiceContract]
public interface IOrderService
{
    // 改进后的幂等方法
    [OperationContract]
    void SafePlaceOrder(Guid requestId, int productId, int quantity);
}

public class OrderService : IOrderService
{
    private static readonly ConcurrentDictionary<Guid, bool> _processedRequests = new();

    public void SafePlaceOrder(Guid requestId, int productId, int quantity)
    {
        // 检查是否已处理过该请求
        if (_processedRequests.TryAdd(requestId, true))
        {
            Inventory.Reduce(productId, quantity);
            Orders.CreateNew(productId, quantity);
        }
        // 已处理过的请求直接忽略
    }
}

二、实现幂等性的五大法宝

1. 唯一请求ID

就像快递单号一样,给每个请求分配唯一标识。服务端记录处理过的ID,遇到重复的直接返回之前的结果:

// 技术栈:C# + WCF
[DataContract]
public class OrderRequest
{
    [DataMember] public Guid RequestId { get; set; }
    [DataMember] public int ProductId { get; set; }
    [DataMember] public int Quantity { get; set; }
}

public class OrderService : IOrderService
{
    public OrderResult ProcessOrder(OrderRequest request)
    {
        // 检查Redis中是否已存在该请求
        if (Redis.Exists($"order:{request.RequestId}"))
            return Redis.Get<OrderResult>($"result:{request.RequestId}");
            
        // 处理新请求
        var result = RealProcessOrder(request);
        
        // 记录处理结果
        Redis.Set($"order:{request.RequestId}", true, TimeSpan.FromDays(1));
        Redis.Set($"result:{request.RequestId}", result, TimeSpan.FromDays(1));
        
        return result;
    }
}

2. 条件更新

利用数据库的原子操作特性,比如:

// 更新库存的幂等方法
public bool ReduceInventory(int productId, int quantity)
{
    // 只有当库存充足时才执行更新
    return DB.Execute(
        "UPDATE Inventory SET Stock = Stock - @quantity " +
        "WHERE ProductId = @productId AND Stock >= @quantity",
        new { productId, quantity }) > 0;
}

3. 状态机控制

让操作变成有状态的,只有处于特定状态时才允许执行:

public void PayOrder(int orderId)
{
    var order = DB.GetOrder(orderId);
    
    // 只有待支付订单才能支付
    if (order.Status != OrderStatus.Pending)
        throw new InvalidOperationException("订单状态异常");
        
    // 执行支付逻辑...
    order.Status = OrderStatus.Paid;
    DB.UpdateOrder(order);
}

4. 去重表

专门建一张表记录已处理的请求:

CREATE TABLE ProcessedRequests (
    RequestId VARCHAR(50) PRIMARY KEY,
    ProcessedTime DATETIME,
    ResultData TEXT
);

5. 乐观并发控制

使用版本号防止重复提交:

public bool UpdateUser(User user)
{
    // 只有当版本号匹配时才更新
    return DB.Execute(
        "UPDATE Users SET Name=@Name, Version=Version+1 " +
        "WHERE Id=@Id AND Version=@Version",
        user) > 0;
}

三、WCF中的最佳实践

WCF服务特别适合采用消息ID方案实现幂等性。我们可以利用消息头传递唯一标识:

// 客户端调用
var proxy = new OrderServiceClient();
using (var scope = new OperationContextScope(proxy.InnerChannel))
{
    // 在消息头中添加唯一ID
    var header = MessageHeader.CreateHeader(
        "MessageId", 
        "urn:services",
        Guid.NewGuid().ToString());
    OperationContext.Current.OutgoingMessageHeaders.Add(header);
    
    proxy.PlaceOrder(1001, 2);
}

// 服务端处理
public class OrderService : IOrderService
{
    public void PlaceOrder(int productId, int quantity)
    {
        // 从消息头获取ID
        var messageId = OperationContext.Current
            .IncomingMessageHeaders
            .GetHeader<string>("MessageId", "urn:services");
            
        // 检查重复
        if (IsDuplicate(messageId)) return;
        
        // 处理逻辑...
    }
}

对于长时间运行的操作,建议实现这样的模式:

[ServiceContract]
public interface IOrderService
{
    [OperationContract]
    Guid BeginPlaceOrder(int productId, int quantity);
    
    [OperationContract]
    OrderStatus CheckOrderStatus(Guid operationId);
}

public class OrderService : IOrderService
{
    public Guid BeginPlaceOrder(int productId, int quantity)
    {
        var operationId = Guid.NewGuid();
        // 将任务放入队列异步处理
        Task.Run(() => ProcessOrderAsync(operationId, productId, quantity));
        return operationId;
    }
    
    private void ProcessOrderAsync(Guid id, int productId, int quantity)
    {
        // 实际处理逻辑...
    }
}

四、常见陷阱与解决方案

  1. 时间窗口问题:请求ID只在短时间内有效

    // 设置合理的过期时间
    Redis.Set($"request:{requestId}", true, TimeSpan.FromMinutes(30));
    
  2. 分布式环境同步:多台服务器如何共享处理状态

    // 使用分布式缓存而不是内存字典
    private readonly IDistributedCache _cache;
    
  3. 错误的重试逻辑:应该区分哪些错误可以重试

    try {
        service.ProcessOrder(request);
    }
    catch (TimeoutException) {
        // 超时可以重试
    }
    catch (InvalidOperationException) {
        // 业务异常不应重试
    }
    
  4. 日志记录过大:避免记录所有请求数据

    // 只记录关键信息
    logger.LogInformation($"Processed order request: {requestId}");
    
  5. 事务处理不当:确保检查和操作在同一个事务中

    using (var transaction = new TransactionScope())
    {
        if (!IsProcessed(requestId))
        {
            Process(request);
            MarkAsProcessed(requestId);
        }
        transaction.Complete();
    }
    

五、实战:完整的订单服务示例

让我们看一个完整的WCF幂等服务实现:

// 技术栈:C# + WCF + SQL Server
[DataContract]
public class OrderRequest
{
    [DataMember] public Guid RequestId { get; set; }
    [DataMember] public int UserId { get; set; }
    [DataMember] public List<OrderItem> Items { get; set; }
}

[ServiceContract]
public interface IOrderService
{
    [OperationContract]
    OrderResult SubmitOrder(OrderRequest request);
}

public class OrderService : IOrderService
{
    private readonly IDbConnection _db;
    private readonly ILogger _logger;

    public OrderResult SubmitOrder(OrderRequest request)
    {
        // 验证基本参数
        if (request == null || request.Items == null || !request.Items.Any())
            throw new ArgumentException("无效的订单请求");
            
        // 检查重复请求
        if (IsDuplicateRequest(request.RequestId))
        {
            _logger.LogWarning($"检测到重复订单请求: {request.RequestId}");
            return GetPreviousResult(request.RequestId);
        }
        
        try
        {
            using (var transaction = _db.BeginTransaction())
            {
                // 检查库存
                foreach (var item in request.Items)
                {
                    var stock = _db.QuerySingle<int>(
                        "SELECT Stock FROM Products WHERE Id = @Id", 
                        new { item.ProductId }, transaction);
                        
                    if (stock < item.Quantity)
                        throw new InsufficientStockException(item.ProductId);
                }
                
                // 扣减库存
                foreach (var item in request.Items)
                {
                    _db.Execute(
                        "UPDATE Products SET Stock = Stock - @Quantity " +
                        "WHERE Id = @ProductId",
                        new { item.ProductId, item.Quantity }, transaction);
                }
                
                // 创建订单
                var orderId = _db.QuerySingle<int>(
                    "INSERT INTO Orders (...) VALUES (...); SELECT SCOPE_IDENTITY();",
                    new { request.UserId, RequestId = request.RequestId },
                    transaction);
                    
                // 记录已处理请求
                _db.Execute(
                    "INSERT INTO ProcessedRequests (RequestId, OrderId) " +
                    "VALUES (@RequestId, @OrderId)",
                    new { request.RequestId, OrderId = orderId },
                    transaction);
                    
                transaction.Commit();
                
                return new OrderResult { Success = true, OrderId = orderId };
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"订单处理失败: {request.RequestId}");
            throw;
        }
    }
    
    private bool IsDuplicateRequest(Guid requestId)
    {
        return _db.QuerySingle<int>(
            "SELECT COUNT(1) FROM ProcessedRequests WHERE RequestId = @RequestId",
            new { requestId }) > 0;
    }
    
    private OrderResult GetPreviousResult(Guid requestId)
    {
        return _db.QuerySingle<OrderResult>(
            "SELECT o.Id as OrderId, 1 as Success " +
            "FROM ProcessedRequests p JOIN Orders o ON p.OrderId = o.Id " +
            "WHERE p.RequestId = @RequestId",
            new { requestId });
    }
}

六、总结与建议

  1. 应用场景

    • 支付系统:防止重复扣款
    • 订单系统:避免重复下单
    • 库存系统:确保扣减操作的准确性
    • 任何可能因网络问题导致重试的业务
  2. 技术优缺点

    • 优点:提高系统可靠性,减少数据不一致
    • 缺点:增加实现复杂度,可能影响性能
  3. 注意事项

    • 请求ID要足够唯一(建议使用Guid)
    • 分布式环境下要使用共享存储
    • 合理设置请求记录的过期时间
    • 考虑性能与一致性的平衡
  4. 最佳实践

    • 为所有写操作设计幂等接口
    • 客户端生成唯一请求ID
    • 服务端实现请求去重
    • 记录详细的操作日志
    • 进行充分的测试(特别是并发测试)

记住,好的幂等设计就像给系统买了保险——平时可能感觉不到它的存在,但关键时刻能避免灾难性后果。从今天开始,为你每个WCF服务加上幂等保护吧!