c# 乐观锁和悲观锁在c#中的实现

9次阅读

乐观锁和悲观锁是并发控制策略而非C#内置语法,需手动设计实现;C#提供原子操作、事务等底层工具,EF Core通过RowVersion+ConcurrencyCheck实现乐观锁,数据库select for UPDATE或UPDLOCK实现悲观锁。

c# 乐观锁和悲观锁在c#中的实现

什么是乐观锁和悲观锁,C# 里为什么不能直接“加锁”这两个概念

乐观锁和悲观锁不是 C# 语言内置的语法或关键字,也不是 lockMonitorSpinLock 这类同步原语。它们是**并发控制策略**,需要你根据场景,在数据访问逻辑中主动设计实现。C# 提供的是底层工具(如原子操作、事务、版本字段、数据库行锁),而不是开箱即用的“乐观锁类”。直接搜 OptimisticLockAttributePessimisticLock 会找不到——因为它们不存在。

用 EF Core 实现乐观锁:靠 ConcurrencyCheckRowVersion

这是最常见、也最推荐的乐观锁落地方式,尤其在 ORM 场景下。核心是让数据库在 UPDATE 时校验数据是否被别人改过。

  • [timestamp][ConcurrencyCheck] 特性标记实体属性(通常是 byte[] RowVersionint Version
  • EF Core 会自动把该字段加入 WHERE 条件,例如:
    UPDATE Products SET Name = @p0, RowVersion = @p1 WHERE Id = @p2 AND RowVersion = @p3
  • 执行后检查 DbContext.SaveChanges() 返回值:如果返回 0,说明 WHERE 不匹配 → 数据已被修改 → 抛出 DbUpdateConcurrencyException
  • 捕获异常后可重试(重新加载再提交)、提示用户或合并变更

手动实现乐观锁:用 Interlocked.CompareExchange 处理整数状态

适用于轻量级共享状态(如计数器、开关标志),不涉及数据库。本质是 CPU 级的 CAS(Compare-And-Swap)操作。

private int _state = 0; // 0=空闲, 1=占用 

public bool TryAcquire() { return Interlocked.CompareExchange(ref _state, 1, 0) == 0; }

public void Release() { Interlocked.Exchange(ref _state, 0); }

注意:Interlocked 只保证单个字段的原子读-改-写,不能保护多字段业务逻辑(比如“扣库存+生成订单”必须用事务或更高层协调)。

悲观锁在 C# 中怎么体现?其实靠的是数据库或外部资源的独占机制

C# 代码本身不“持有悲观锁”,而是通过调用数据库的 SELECT ... FOR UPDATEmysql/postgresql)或 WITH (UPDLOCK)(SQL Server)来让数据库行加锁,阻塞其他事务读写。EF Core 不直接支持原生写法,需用 FromSqlRaw 或 ADO.net

  • 使用 SqlConnection.BeginTransaction(IsolationLevel.RepeatableRead)Serializable 提升隔离级别,间接增强锁力度
  • 在事务中执行带锁查询:
    SELECT Id, Stock FROM Products WHERE Id = 123 WITH (UPDLOCK, ROWLOCK)
  • 锁持续到事务结束(CommitRollback),期间其他事务对该行的 UPDATE/SELECT FOR UPDATE 会被阻塞
  • 滥用会导致死锁、响应延迟——必须严格控制事务粒度和时长

真正容易被忽略的是:乐观锁失败后重试逻辑是否幂等;悲观锁的事务范围是否意外包含 UI 等待或远程调用;以及 RowVersion 字段在迁移中是否被 EF Core 正确识别为并发令牌。

text=ZqhQzanResources