并发冲突指的是多个操作在同一时间段内同时访问并修改同一份数据资源,最终导致数据状态不符合预期的问题。在数据库操作中,这类问题会直接影响业务数据的准确性,是后端开发中需要重点处理的技术点。

并发冲突的产生场景
最常见的并发冲突场景是两个用户同时查询并修改同一条数据:
- 用户A和用户B同时查询到某商品的库存为10
- 用户A提交订单,将库存减1,此时数据库库存变为9
- 用户B也提交订单,基于自己查询到的10库存减1,将库存更新为9
- 最终库存实际应该是8,但是被错误更新为9,产生了数据不一致
C#中处理数据库并发的两种核心方案
1. 悲观锁方案
悲观锁的核心思路是假设并发冲突一定会发生,在操作数据前先对数据加锁,其他操作需要等待锁释放才能访问该数据。在C#中使用EF Core时,可以通过DbContext的事务结合数据库的锁机制实现悲观锁。
以下是使用SQL Server的悲观锁实现示例:
using (var context = new AppDbContext())
{
using (var transaction = context.Database.BeginTransaction())
{
// 查询数据时加排他锁,其他事务无法修改该数据
var product = context.Products
.FromSqlRaw("SELECT * FROM Products WITH (XLOCK, ROWLOCK) WHERE Id = {0}", 1)
.FirstOrDefault();
if (product != null && product.Stock > 0)
{
product.Stock -= 1;
context.SaveChanges();
}
transaction.Commit();
}
}
悲观锁的优点是能够完全避免并发冲突,缺点是会阻塞其他操作,高并发场景下性能较差,适合并发量不高、数据一致性要求极高的场景。
2. 乐观锁方案
乐观锁假设并发冲突不会频繁发生,不在操作前加锁,而是在更新数据时判断数据是否被其他操作修改过。EF Core原生支持乐观锁,最常用的实现方式是给数据表添加RowVersion时间戳字段。
首先定义包含RowVersion的实体类:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public int Stock { get; set; }
// 时间戳字段,EF Core会自动维护该字段的值
public byte[] RowVersion { get; set; }
}
然后在DbContext中配置该字段为并发令牌:
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.Property(p => p.RowVersion)
.IsRowVersion(); // 标记为并发令牌
}
}
执行更新操作时,EF Core会自动在更新语句中加入RowVersion的判断,如果数据被其他操作修改过,就会抛出DbUpdateConcurrencyException异常,我们可以在异常中处理冲突逻辑:
using (var context = new AppDbContext())
{
var product = context.Products.Find(1);
if (product == null) return;
try
{
product.Stock -= 1;
context.SaveChanges(); // 如果RowVersion不匹配,这里会抛出异常
}
catch (DbUpdateConcurrencyException ex)
{
// 处理并发冲突:可以重新查询最新数据重试,或者提示用户数据已被修改
var entry = ex.Entries.Single();
var databaseValues = entry.GetDatabaseValues();
if (databaseValues != null)
{
// 获取数据库中的最新库存
var latestStock = (int)databaseValues["Stock"];
if (latestStock > 0)
{
// 基于最新数据重试更新
entry.CurrentValues.SetValues(databaseValues);
((Product)entry.Entity).Stock -= 1;
context.SaveChanges();
}
}
}
}
乐观锁的优点是性能较好,不会阻塞其他操作,缺点是冲突发生时需要处理重试逻辑,适合高并发、冲突概率较低的场景。
两种方案的选型建议
| 对比维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 并发性能 | 低,会阻塞其他操作 | 高,无阻塞 |
| 实现复杂度 | 较低,依赖数据库锁机制 | 中等,需要处理冲突异常 |
| 适用场景 | 并发量低、数据一致性要求极高 | 高并发、冲突概率低 |
在实际C#开发中,大部分业务场景优先选择乐观锁方案,只有在数据一致性要求极高且并发量很小的场景下才考虑使用悲观锁。同时需要注意,无论使用哪种方案,都要做好异常处理,避免并发冲突导致业务功能异常。