Skip to content

并发冲突解决 ​

概述 ​

当多个用户同时修改同一条数据时,会发生并发冲突。EF Core 提供乐观并发(Optimistic Concurrency)机制来检测和处理这类冲突。


实现乐观并发 ​

方法 1: 使用 RowVersion (推荐) ​

csharp
// Domain/Entities/Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    
    // 并发令牌
    [Timestamp]
    public byte[] RowVersion { get; set; } = Array.Empty<byte>();
}

// 或 Fluent API 配置
modelBuilder.Entity<Product>(entity =>
{
    entity.Property(p => p.RowVersion)
        .IsRowVersion(); // SQL Server
});

方法 2: 使用 ConcurrencyCheck ​

csharp
public class Product
{
    public int Id { get; set; }
    
    [ConcurrencyCheck]
    public decimal Price { get; set; }
}

处理并发异常 ​

csharp
public async Task UpdatePriceAsync(int productId, decimal newPrice)
{
    var product = await _context.Products.FindAsync(productId);
    if (product == null) return;
    
    product.Price = newPrice;
    
    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException ex)
    {
        // 获取冲突的实体
        var entry = ex.Entries.Single();
        var databaseValues = await entry.GetDatabaseValuesAsync();
        var clientValues = entry.Entity;
        
        // 策略 1: 数据库优先(放弃客户端更改)
        entry.OriginalValues.SetValues(databaseValues);
        await _context.SaveChangesAsync();
        
        // 策略 2: 客户端优先(覆盖数据库)
        // entry.OriginalValues.SetValues(clientValues);
        // await _context.SaveChangesAsync();
        
        // 策略 3: 合并更改
        // ResolveConflicts(entry, databaseValues, clientValues);
    }
}

private void ResolveConflicts(EntityEntry entry, PropertyValues databaseValues, object clientEntity)
{
    var clientValues = entry.CurrentValues.Clone();
    
    // 遍历所有属性
    foreach (var property in entry.Metadata.GetProperties())
    {
        var databaseValue = databaseValues[property.Name];
        var clientValue = clientValues[property.Name];
        var originalValue = entry.OriginalValues[property.Name];
        
        // 如果数据库值和原始值不同,说明被其他人修改过
        if (!Equals(databaseValue, originalValue))
        {
            // 如果客户端也修改了,需要业务逻辑决定如何合并
            if (!Equals(clientValue, originalValue))
            {
                // 示例: 数值类型取最大值
                if (property.ClrType == typeof(decimal))
                {
                    var dbDecimal = (decimal)databaseValue;
                    var clientDecimal = (decimal)clientValue;
                    entry.CurrentValues[property.Name] = Math.Max(dbDecimal, clientDecimal);
                }
                else
                {
                    // 其他类型: 客户端优先
                    entry.CurrentValues[property.Name] = clientValue;
                }
            }
            else
            {
                // 客户端未修改,使用数据库值
                entry.CurrentValues[property.Name] = databaseValue;
            }
        }
    }
}

Web API 中的并发处理 ​

csharp
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, UpdateProductRequest request)
{
    var product = await _context.Products.FindAsync(id);
    if (product == null)
        return NotFound();
    
    // 从请求头获取原始的 RowVersion
    if (Request.Headers.TryGetValue("If-Match", out var etag))
    {
        var originalRowVersion = Convert.FromBase64String(etag.ToString());
        _context.Entry(product).Property(p => p.RowVersion).OriginalValue = originalRowVersion;
    }
    
    product.Name = request.Name;
    product.Price = request.Price;
    
    try
    {
        await _context.SaveChangesAsync();
        
        // 返回新的 ETag
        var newEtag = Convert.ToBase64String(product.RowVersion);
        Response.Headers["ETag"] = newEtag;
        
        return NoContent();
    }
    catch (DbUpdateConcurrencyException)
    {
        return Conflict(new 
        { 
            error = "The record was modified by another user",
            currentData = product
        });
    }
}

前端调用:

javascript
// 1. 获取数据和 ETag
const response = await fetch('/api/products/1');
const product = await response.json();
const etag = response.headers.get('ETag');

// 2. 更新时带上 ETag
await fetch('/api/products/1', {
    method: 'PUT',
    headers: {
        'Content-Type': 'application/json',
        'If-Match': etag // 并发检查
    },
    body: JSON.stringify({ name: 'Updated', price: 120 })
});

最佳实践 ​

✅ 推荐做法 ​

  1. 始终使用 RowVersion: 比单独字段更可靠
  2. 友好的错误提示: 告诉用户如何解决冲突
  3. 自动重试: 对于非关键操作,可以自动重试
  4. 记录日志: 监控并发冲突频率

❌ 避免的陷阱 ​

  1. 不要忽略异常: 会导致数据丢失
  2. 不要盲目覆盖: 可能破坏其他人的修改
  3. 不要忘记设置 OriginalValue: 否则无法检测冲突

总结 ​

策略适用场景复杂度
数据库优先审计日志、不可变数据⭐ 低
客户端优先用户配置、个人数据⭐ 低
合并更改协作编辑、订单系统⭐⭐⭐ 高

建议: 大多数场景使用 RowVersion + 客户端优先策略!

基于 MIT 许可发布