c# 数据库乐观锁和悲观锁哪个好 c#如何选择

1次阅读

乐观锁适合读多写少场景,悲观锁适合高冲突写密集操作;c#中ef core通过[timestamp]实现乐观锁,select for update实现悲观锁,选择取决于业务并发特征与一致性要求。

c# 数据库乐观锁和悲观锁哪个好 c#如何选择

乐观锁适合读多写少的 C# 场景,悲观锁适合高冲突、强一致性的写密集操作

没有“哪个更好”,只有“哪个更合适”。C# 中数据库锁的选择本质是权衡:你愿不愿意为数据安全多等几毫秒,还是愿意承担重试失败的风险来换吞吐量?关键看你的业务场景中 update 操作是否频繁、并发用户是否集中修改同一批记录(比如订单状态、库存扣减)、能否接受失败后重试或提示用户刷新。

C# 用 Entity Framework 实现乐观锁:加 Version 字段 + ConcurrencyCheck

这是最常用也最稳妥的乐观锁落地方式。EF Core 会自动在 UPDATEWHERE 子句中带上版本号校验,避免覆盖式更新。

  • 实体类里加一个 [Timestamp][ConcurrencyCheck] 标记的 byte[] Version 字段(sql Server 推荐 rowversion 类型)
  • EF Core 迁移后,数据库该字段会自动生成并随每次更新递增
  • 执行 SaveChanges() 时若发现 WHERE 条件不匹配(即版本号已变),抛出 DbUpdateConcurrencyException
  • 你必须捕获这个异常,并决定是重试、合并、还是提示用户“数据已被他人修改”
public class Order {     public int Id { get; set; }     public string Status { get; set; }     [Timestamp] // 自动映射为 rowversion,EF 会用于并发检查     public byte[] Version { get; set; } }

C# 用 SELECT ... FOR UPDATE 做悲观锁:只在事务内有效,且依赖数据库支持

这不是 C# 语言级的锁,而是通过 EF 或原生 ADO.NET 向数据库发出带锁的查询指令。适用于“查-改-存”三步不能拆开、且冲突概率高的场景(如秒杀库存扣减)。

  • 必须在显式事务中执行:BeginTransaction(IsolationLevel.RepeatableRead) 或更高
  • 用原始 SQL 执行 SELECT * FROM order_table WHERE id = @id FOR UPDATEmysql/postgresql)或 WITH (UPDLOCK, ROWLOCK)(SQL Server)
  • EF Core 本身不直接支持 FOR UPDATE,需用 database.ExecuteSqlRaw()FromSqlRaw()
  • 注意:锁会持续到事务结束,超时或未提交易导致阻塞甚至死锁

容易踩的坑:别把乐观锁当“免死金牌”,也别让悲观锁拖垮响应

很多团队误以为加了 [Timestamp] 就万事大吉——但没处理 DbUpdateConcurrencyException,结果更新静默失败;也有团队一上来就全用 FOR UPDATE,结果高峰期大量请求卡在锁等待,接口 P99 直接翻倍。

  • 乐观锁重试不能无脑循环:要设上限(比如最多 3 次),并考虑加入随机退避(Task.Delay(Random.Shared.Next(10, 100))),防止雪崩重试
  • 悲观锁务必配超时:command.CommandTimeout = 10,否则一个卡住的事务可能拖垮整个连接池
  • 混合使用常见但难维护:比如主流程用乐观锁,关键资金操作切悲观锁——这时要确保事务边界清晰,避免跨上下文锁升级失败

真正难的不是选锁,而是定义清楚“哪条数据谁在什么时候可能被谁改”。画一张状态流转图,标出所有并发修改点,再挨个决定用哪种锁——这比背概念管用十倍。

text=ZqhQzanResources