Appearance
处理 DbUpdateConcurrencyException
并发冲突的完整处理方案和最佳实践
📖 目录
异常详解
何时抛出
csharp
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// 当 UPDATE/DELETE 语句影响 0 行时抛出
// 原因: WHERE 条件中的并发令牌不匹配
}触发条件:
- 使用了
[Timestamp]或IsRowVersion() - 数据库中的 RowVersion 已改变
- WHERE 子句:
WHERE Id = @id AND RowVersion = @original - 影响行数 = 0 → 抛出异常
异常结构
csharp
public class DbUpdateConcurrencyException : DbUpdateException
{
// 冲突的实体条目
public IReadOnlyList<EntityEntry> Entries { get; }
// 使用方法
foreach (var entry in ex.Entries)
{
var entityType = entry.Entity.GetType().Name;
var state = entry.State;
// 获取值
var currentValues = entry.CurrentValues;
var originalValues = entry.OriginalValues;
var databaseValues = entry.GetDatabaseValues();
}
}三种值对比
csharp
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
// 1. 原始值(查询时的值)
var originalPrice = entry.OriginalValues["Price"]; // 100
// 2. 当前值(客户端修改后的值)
var currentPrice = entry.CurrentValues["Price"]; // 120
// 3. 数据库值(其他用户修改的值)
var databaseValues = entry.GetDatabaseValues();
var dbPrice = databaseValues["Price"]; // 80
Console.WriteLine($"Original: {originalPrice}");
Console.WriteLine($"Current (Client): {currentPrice}");
Console.WriteLine($"Database: {dbPrice}");
}
}冲突解决策略
策略 1: 客户端获胜(Client Wins)
适用场景: 用户的修改更重要,最后写入获胜
csharp
public async Task UpdateProductAsync(int id, ProductDto dto)
{
var product = await context.Products.FindAsync(id);
if (product == null) throw new NotFoundException();
product.Name = dto.Name;
product.Price = dto.Price;
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
// 刷新原始值为数据库最新值
entry.OriginalValues.SetValues(entry.GetDatabaseValues());
}
// 重试保存(客户端值覆盖数据库)
await context.SaveChangesAsync();
}
}工作流程:
T1: A 读取 (Price=100, RowVersion=v1)
T2: B 读取 (Price=100, RowVersion=v1)
T3: A 修改为 120, 保存成功 (Price=120, RowVersion=v2)
T4: B 修改为 80, 失败 (RowVersion 不匹配)
T5: B 刷新 OriginalValues=v2, 重试
T6: B 保存成功 (Price=80, RowVersion=v3) ✅ 客户端获胜策略 2: 数据库获胜(Store Wins)
适用场景: 数据完整性优先,需要人工审查
csharp
public async Task<ProductDto> UpdateProductAsync(int id, ProductDto dto)
{
var product = await context.Products.FindAsync(id);
if (product == null) throw new NotFoundException();
product.Name = dto.Name;
product.Price = dto.Price;
try
{
await context.SaveChangesAsync();
return MapToDto(product);
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
// 刷新为数据库的最新值
var databaseValues = entry.GetDatabaseValues();
entry.CurrentValues.SetValues(databaseValues);
}
// 返回最新数据,让客户端决定
var updatedProduct = entry.Entity as Product;
throw new ConcurrencyConflictException(
"Record was modified by another user",
MapToDto(updatedProduct));
}
}前端处理:
javascript
try {
await updateProduct(id, data);
} catch (error) {
if (error.status === 409) {
// 显示冲突对话框
const result = await showConflictDialog(error.latestData);
if (result.action === 'overwrite') {
// 用户选择覆盖,重新提交
await updateProduct(id, { ...data, rowVersion: error.latestData.rowVersion });
} else if (result.action === 'refresh') {
// 用户选择刷新,重新加载
await loadProduct(id);
}
}
}策略 3: 合并变更(智能合并)
适用场景: 不同字段可独立修改
csharp
public async Task UpdateProductAsync(int id, ProductDto dto)
{
var product = await context.Products.FindAsync(id);
if (product == null) throw new NotFoundException();
product.Name = dto.Name;
product.Price = dto.Price;
product.StockQuantity = dto.StockQuantity;
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
var proposedValues = entry.CurrentValues;
var databaseValues = entry.GetDatabaseValues();
var originalValues = entry.OriginalValues;
// 逐字段检查
foreach (var property in proposedValues.Properties)
{
var proposedValue = proposedValues[property];
var databaseValue = databaseValues[property];
var originalValue = originalValues[property];
// 情况 1: 只有客户端修改
if (!Equals(proposedValue, originalValue) &&
Equals(databaseValue, originalValue))
{
// 使用客户端值(保持不变)
}
// 情况 2: 只有数据库修改
else if (Equals(proposedValue, originalValue) &&
!Equals(databaseValue, originalValue))
{
// 使用数据库值
entry.Property(property.Name).CurrentValue = databaseValue;
}
// 情况 3: 都修改了同一字段(冲突!)
else if (!Equals(proposedValue, originalValue) &&
!Equals(databaseValue, originalValue))
{
// 冲突!需要特殊处理
if (property.Name == "StockQuantity")
{
// 库存: 累加变化量
var clientChange = (int)proposedValue - (int)originalValue;
var mergedValue = (int)databaseValue + clientChange;
entry.Property(property.Name).CurrentValue = mergedValue;
}
else
{
// 其他字段: 抛出冲突异常
throw new ConcurrencyConflictException(
$"Conflict on {property.Name}: " +
$"Client={proposedValue}, DB={databaseValue}");
}
}
// 情况 4: 都没修改
else
{
// 保持不变
}
}
}
// 重试保存
await context.SaveChangesAsync();
}
}示例场景:
原始: Price=100, Stock=50
客户端修改: Price=120, Stock=45 (卖出5个)
数据库修改: Stock=48 (其他人卖出2个)
合并结果:
- Price: 120 (只有客户端修改)
- Stock: 43 (48 - 5 = 43, 累加变化量)策略 4: 重试策略
适用场景: 临时冲突,高并发场景
csharp
public async Task UpdateWithRetryAsync(int productId, decimal newPrice, int maxRetries = 3)
{
for (int retry = 0; retry < maxRetries; retry++)
{
try
{
var product = await context.Products.FindAsync(productId);
if (product == null) throw new NotFoundException();
product.Price = newPrice;
await context.SaveChangesAsync();
return; // 成功
}
catch (DbUpdateConcurrencyException)
{
if (retry == maxRetries - 1)
{
throw; // 最后一次仍失败,抛出异常
}
// 指数退避重试
var delay = TimeSpan.FromMilliseconds(100 * Math.Pow(2, retry));
await Task.Delay(delay);
}
}
}重试时间:
- 第 1 次: 立即
- 第 2 次: 等待 100ms
- 第 3 次: 等待 200ms
- 第 4 次: 等待 400ms
API 实现
Minimal API 完整示例
csharp
app.Put("/api/products/{id}", async (int id, ProductUpdateDto dto, AppDbContext context) =>
{
var product = await context.Products.FindAsync(id);
if (product == null)
return Results.NotFound();
// 检查 RowVersion
if (dto.RowVersion != null && !dto.RowVersion.SequenceEqual(product.RowVersion))
{
return Results.Conflict(new
{
error = "concurrency_conflict",
message = "This record has been modified by another user",
currentData = new
{
product.Id,
product.Name,
product.Price,
product.RowVersion
},
action = "refresh_and_retry"
});
}
product.Name = dto.Name;
product.Price = dto.Price;
try
{
await context.SaveChangesAsync();
return Results.NoContent();
}
catch (DbUpdateConcurrencyException)
{
// 重新查询最新数据
var latestProduct = await context.Products.FindAsync(id);
if (latestProduct == null)
return Results.NotFound();
return Results.Conflict(new
{
error = "concurrency_conflict",
message = "This record was just modified by another user",
currentData = new
{
latestProduct.Id,
latestProduct.Name,
latestProduct.Price,
latestProduct.RowVersion
},
action = "refresh_and_retry"
});
}
});Controller API 示例
csharp
[HttpPut("{id}")]
public async Task<ActionResult> UpdateProduct(int id, ProductUpdateDto dto)
{
var product = await _context.Products.FindAsync(id);
if (product == null)
return NotFound();
// 乐观并发检查
if (dto.RowVersion != null && !dto.RowVersion.SequenceEqual(product.RowVersion))
{
ModelState.AddModelError("RowVersion", "The record has been modified by another user.");
return Conflict(new
{
errors = ModelState.ToDictionary(
kv => kv.Key,
kv => kv.Value.Errors.Select(e => e.ErrorMessage).ToArray()
),
latestData = product
});
}
product.Name = dto.Name;
product.Price = dto.Price;
try
{
await _context.SaveChangesAsync();
return NoContent();
}
catch (DbUpdateConcurrencyException ex)
{
if (!ProductExists(id))
{
return NotFound();
}
else
{
_logger.LogWarning(ex, "Concurrency conflict on product {ProductId}", id);
var latestProduct = await _context.Products.FindAsync(id);
return Conflict(new
{
error = "concurrency_conflict",
message = "Please refresh and try again",
latestData = latestProduct
});
}
}
}前端处理
JavaScript/TypeScript 示例
typescript
interface Product {
id: number;
name: string;
price: number;
rowVersion: number[];
}
async function updateProduct(id: number, data: Partial<Product>): Promise<void> {
try {
const response = await fetch(`/api/products/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.status === 409) {
// 并发冲突
const error = await response.json();
// 显示冲突对话框
const userChoice = await showConflictDialog(
error.message,
error.latestData
);
if (userChoice === 'overwrite') {
// 用户选择覆盖,使用最新 RowVersion 重新提交
await updateProduct(id, {
...data,
rowVersion: error.latestData.rowVersion
});
} else if (userChoice === 'refresh') {
// 用户选择刷新,重新加载数据
await loadProduct(id);
}
// 'cancel' - 什么都不做
} else if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
} catch (error) {
console.error('Update failed:', error);
showErrorNotification('Failed to update product');
}
}
// 冲突对话框
function showConflictDialog(message: string, latestData: Product): Promise<string> {
return new Promise((resolve) => {
// 显示模态对话框
const dialog = document.createElement('div');
dialog.innerHTML = `
<div class="conflict-dialog">
<h3>⚠️ Concurrent Modification Detected</h3>
<p>${message}</p>
<div class="data-comparison">
<h4>Latest Data:</h4>
<pre>${JSON.stringify(latestData, null, 2)}</pre>
</div>
<div class="actions">
<button onclick="resolve('overwrite')">Overwrite</button>
<button onclick="resolve('refresh')">Refresh</button>
<button onclick="resolve('cancel')">Cancel</button>
</div>
</div>
`;
document.body.appendChild(dialog);
});
}React Hook 示例
typescript
import { useState, useCallback } from 'react';
function useProductUpdater(productId: number) {
const [isUpdating, setIsUpdating] = useState(false);
const [conflict, setConflict] = useState<Product | null>(null);
const updateProduct = useCallback(async (data: Partial<Product>) => {
setIsUpdating(true);
setConflict(null);
try {
const response = await fetch(`/api/products/${productId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.status === 409) {
const error = await response.json();
setConflict(error.latestData);
return { success: false, conflict: true };
}
if (!response.ok) {
throw new Error('Update failed');
}
return { success: true };
} catch (error) {
console.error('Update failed:', error);
return { success: false, error };
} finally {
setIsUpdating(false);
}
}, [productId]);
const resolveConflict = useCallback(async (action: 'overwrite' | 'refresh') => {
if (action === 'overwrite' && conflict) {
// 使用最新 RowVersion 重新提交
await updateProduct({
// ... 用户的数据
rowVersion: conflict.rowVersion
});
} else if (action === 'refresh') {
// 刷新数据
window.location.reload();
}
setConflict(null);
}, [conflict, updateProduct]);
return { updateProduct, isUpdating, conflict, resolveConflict };
}测试方法
单元测试
csharp
[Fact]
public async Task ConcurrentUpdates_ShouldDetectConflict()
{
// Arrange
var product = new Product { Name = "Test", Price = 100 };
context.Products.Add(product);
await context.SaveChangesAsync();
// 创建两个独立的上下文
var context1 = CreateNewContext();
var context2 = CreateNewContext();
// Act
var product1 = await context1.Products.FindAsync(product.Id);
var product2 = await context2.Products.FindAsync(product.Id);
product1.Price = 120;
await context1.SaveChangesAsync(); // 成功
product2.Price = 80;
// Assert
var exception = await Assert.ThrowsAsync<DbUpdateConcurrencyException>(
() => context2.SaveChangesAsync());
Assert.Single(exception.Entries);
}集成测试
csharp
[Fact]
public async Task Api_UpdateProduct_WithConcurrencyConflict_Returns409()
{
// Arrange
var product = new Product { Name = "Test", Price = 100 };
context.Products.Add(product);
await context.SaveChangesAsync();
var client1 = factory.CreateClient();
var client2 = factory.CreateClient();
// Act - 两个客户端同时更新
var task1 = client1.PutAsJsonAsync($"/api/products/{product.Id}", new
{
name = "Updated by Client 1",
price = 120,
rowVersion = product.RowVersion
});
var task2 = client2.PutAsJsonAsync($"/api/products/{product.Id}", new
{
name = "Updated by Client 2",
price = 80,
rowVersion = product.RowVersion
});
var response1 = await task1;
var response2 = await task2;
// Assert
Assert.True(response1.IsSuccessStatusCode || response2.IsSuccessStatusCode);
Assert.True(response1.StatusCode == HttpStatusCode.Conflict ||
response2.StatusCode == HttpStatusCode.Conflict);
}最佳实践
1. 始终返回 RowVersion
csharp
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public byte[] RowVersion { get; set; } // ✅ 必须返回
}2. 提供友好的错误信息
csharp
return Conflict(new
{
error = "concurrency_conflict",
message = "This record has been modified by another user. Please review the latest changes and try again.",
action = "refresh_and_retry",
latestData: product
});3. 记录并发冲突
csharp
catch (DbUpdateConcurrencyException ex)
{
_logger.LogWarning(ex,
"Concurrency conflict detected for {EntityType} with ID {Id}",
entry.Entity.GetType().Name,
GetEntityId(entry));
// 处理冲突...
}4. 设置合理的重试次数
csharp
// ✅ 推荐: 最多 3 次
for (int i = 0; i < 3; i++) { ... }
// ❌ 避免: 无限重试
while (true) { ... }5. 前端防抖
javascript
// 防止用户快速连续点击保存
let saveTimeout;
function handleSave() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
updateProduct(id, data);
}, 300);
}💡 小结
核心要点:
- ✅ DbUpdateConcurrencyException 在并发令牌不匹配时抛出
- ✅ 四种解决策略: 客户端获胜/数据库获胜/合并/重试
- ✅ API 返回 RowVersion 给客户端
- ✅ 提供友好的错误信息和操作指引
- ✅ 前端显示冲突对话框,让用户选择
- ✅ 记录并发冲突日志
处理流程:
捕获异常 → 获取三方值 → 选择策略 → 处理冲突 → 重试或返回下一步: