在C#开发的多用户业务系统中,当多个用户同时读取并修改同一条数据库记录时,很容易出现并发冲突,比如后提交的操作覆盖了先提交操作的结果,导致数据不一致。EF Core作为常用的ORM框架,提供了乐观并发控制机制来应对这类问题,不需要长时间锁定数据库资源,性能表现更优。

乐观并发控制的核心原理
乐观并发控制的核心假设是并发冲突发生的概率较低,因此在数据读取时不会加锁,只在数据提交更新时检查数据是否被其他操作修改过。如果检测到数据已经被修改,就会抛出并发异常,由开发者决定后续的处理逻辑,比如重试操作、提示用户数据已被修改等。
EF Core中配置乐观并发令牌
要实现乐观并发控制,首先需要在实体中配置并发令牌,EF Core会在更新语句的WHERE条件中带上并发令牌的原始值,如果更新时令牌值和数据库中的值不匹配,就会判定为并发冲突。
使用Timestamp/RowVersion配置并发令牌
这是最常用的方式,数据库会自动维护这个字段的值,每次更新记录时该字段会自动变化。
// 定义实体类,配置并发令牌
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
// 配置为并发令牌,数据库会生成timestamp类型或者rowversion类型
public byte[] RowVersion { get; set; }
}
// 在DbContext中配置实体
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 将RowVersion配置为并发令牌
modelBuilder.Entity<Product>()
.Property(p => p.RowVersion)
.IsRowVersion();
}
}
使用普通属性作为并发令牌
也可以使用业务属性作为并发令牌,比如库存数量,每次更新时检查库存是否和读取时一致。
public class Inventory
{
public int Id { get; set; }
public string GoodsName { get; set; }
public int StockCount { get; set; }
}
public class AppDbContext : DbContext
{
public DbSet<Inventory> Inventories { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 将StockCount配置为并发令牌
modelBuilder.Entity<Inventory>()
.Property(i => i.StockCount)
.IsConcurrencyToken();
}
}
处理并发冲突异常
当发生并发冲突时,EF Core会抛出DbUpdateConcurrencyException异常,我们可以通过捕获这个异常来处理冲突逻辑。
基础并发冲突处理示例
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
public class ProductService
{
private readonly AppDbContext _dbContext;
public ProductService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public void UpdateProductPrice(int productId, decimal newPrice)
{
try
{
var product = _dbContext.Products.FirstOrDefault(p => p.Id == productId);
if (product == null)
{
Console.WriteLine("商品不存在");
return;
}
// 模拟其他用户修改了数据
// _dbContext.Database.ExecuteSqlRaw("UPDATE Products SET Price = 100 WHERE Id = {0}", productId);
product.Price = newPrice;
_dbContext.SaveChanges();
Console.WriteLine("价格更新成功");
}
catch (DbUpdateConcurrencyException ex)
{
Console.WriteLine("发生并发冲突,数据已被其他用户修改");
// 获取当前数据库中的实体值
var databaseValues = ex.Entries.Single().GetDatabaseValues();
if (databaseValues == null)
{
Console.WriteLine("数据已被其他用户删除");
return;
}
// 输出数据库中的最新值
var dbProduct = (Product)databaseValues.ToObject();
Console.WriteLine($"数据库最新价格:{dbProduct.Price}");
// 可以选择重试更新,或者提示用户
}
}
}
自定义并发冲突解决策略
我们可以实现自定义的冲突解决逻辑,比如合并当前修改和数据库最新值,或者强制覆盖。
public void UpdateProductWithConflictResolve(int productId, decimal newPrice)
{
bool saveFailed;
do
{
saveFailed = false;
try
{
var product = _dbContext.Products.FirstOrDefault(p => p.Id == productId);
if (product == null)
{
Console.WriteLine("商品不存在");
return;
}
product.Price = newPrice;
_dbContext.SaveChanges();
Console.WriteLine("更新成功");
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true;
var entry = ex.Entries.Single();
// 获取数据库当前值
var databaseValues = entry.GetDatabaseValues();
// 获取当前上下文中的原始值
var originalValues = entry.OriginalValues;
// 获取当前要提交的值
var currentValues = entry.CurrentValues;
// 自定义解决策略:如果价格比数据库中的低,就使用数据库的价格,否则使用当前要提交的价格
var dbPrice = (decimal)databaseValues["Price"];
var currentPrice = (decimal)currentValues["Price"];
if (currentPrice < dbPrice)
{
currentValues["Price"] = dbPrice;
}
// 更新原始值为数据库当前值,重试保存
entry.OriginalValues.SetValues(databaseValues);
}
} while (saveFailed);
}
乐观并发的适用场景
乐观并发适合并发冲突发生概率较低的场景,比如大部分操作是读取,修改操作较少的业务场景。如果业务中并发冲突非常频繁,乐观并发会导致大量的重试和异常,此时可以考虑使用悲观并发控制,但是悲观并发会长时间锁定资源,影响系统性能,需要根据实际业务情况选择。
注意事项
- 并发令牌的字段不要手动修改,避免令牌值异常导致并发控制失效。
- 处理并发异常时,不要忽略异常直接重试,需要根据业务需求设计合理的解决逻辑。
- 如果使用普通属性作为并发令牌,要确保该属性每次更新都会变化,否则无法检测到并发冲突。