Skip to content

处理 DbUpdateConcurrencyException ​

并发冲突的完整处理方案和最佳实践

📖 目录 ​


异常详解 ​

何时抛出 ​

csharp
try
{
    await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
    // 当 UPDATE/DELETE 语句影响 0 行时抛出
    // 原因: WHERE 条件中的并发令牌不匹配
}

触发条件:

  1. 使用了 [Timestamp] 或 IsRowVersion()
  2. 数据库中的 RowVersion 已改变
  3. WHERE 子句: WHERE Id = @id AND RowVersion = @original
  4. 影响行数 = 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 给客户端
  • ✅ 提供友好的错误信息和操作指引
  • ✅ 前端显示冲突对话框,让用户选择
  • ✅ 记录并发冲突日志

处理流程:

捕获异常 → 获取三方值 → 选择策略 → 处理冲突 → 重试或返回

下一步:

  1. 学习 悲观并发控制
  2. 掌握 事务隔离级别
  3. 理解 性能监控

基于 MIT 许可发布