一、什么是幂等性?为什么需要它?
想象一下你去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)
{
// 实际处理逻辑...
}
}
四、常见陷阱与解决方案
时间窗口问题:请求ID只在短时间内有效
// 设置合理的过期时间 Redis.Set($"request:{requestId}", true, TimeSpan.FromMinutes(30));分布式环境同步:多台服务器如何共享处理状态
// 使用分布式缓存而不是内存字典 private readonly IDistributedCache _cache;错误的重试逻辑:应该区分哪些错误可以重试
try { service.ProcessOrder(request); } catch (TimeoutException) { // 超时可以重试 } catch (InvalidOperationException) { // 业务异常不应重试 }日志记录过大:避免记录所有请求数据
// 只记录关键信息 logger.LogInformation($"Processed order request: {requestId}");事务处理不当:确保检查和操作在同一个事务中
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 });
}
}
六、总结与建议
应用场景:
- 支付系统:防止重复扣款
- 订单系统:避免重复下单
- 库存系统:确保扣减操作的准确性
- 任何可能因网络问题导致重试的业务
技术优缺点:
- 优点:提高系统可靠性,减少数据不一致
- 缺点:增加实现复杂度,可能影响性能
注意事项:
- 请求ID要足够唯一(建议使用Guid)
- 分布式环境下要使用共享存储
- 合理设置请求记录的过期时间
- 考虑性能与一致性的平衡
最佳实践:
- 为所有写操作设计幂等接口
- 客户端生成唯一请求ID
- 服务端实现请求去重
- 记录详细的操作日志
- 进行充分的测试(特别是并发测试)
记住,好的幂等设计就像给系统买了保险——平时可能感觉不到它的存在,但关键时刻能避免灾难性后果。从今天开始,为你每个WCF服务加上幂等保护吧!
评论