Appearance
并发冲突解决
概述
当多个用户同时修改同一条数据时,会发生并发冲突。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 })
});最佳实践
✅ 推荐做法
- 始终使用 RowVersion: 比单独字段更可靠
- 友好的错误提示: 告诉用户如何解决冲突
- 自动重试: 对于非关键操作,可以自动重试
- 记录日志: 监控并发冲突频率
❌ 避免的陷阱
- 不要忽略异常: 会导致数据丢失
- 不要盲目覆盖: 可能破坏其他人的修改
- 不要忘记设置 OriginalValue: 否则无法检测冲突
总结
| 策略 | 适用场景 | 复杂度 |
|---|---|---|
| 数据库优先 | 审计日志、不可变数据 | ⭐ 低 |
| 客户端优先 | 用户配置、个人数据 | ⭐ 低 |
| 合并更改 | 协作编辑、订单系统 | ⭐⭐⭐ 高 |
建议: 大多数场景使用 RowVersion + 客户端优先策略!