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

乐观锁适合读多写少的 C# 场景,悲观锁适合高冲突、强一致性的写密集操作
没有“哪个更好”,只有“哪个更合适”。C# 中数据库锁的选择本质是权衡:你愿不愿意为数据安全多等几毫秒,还是愿意承担重试失败的风险来换吞吐量?关键看你的业务场景中 update 操作是否频繁、并发用户是否集中修改同一批记录(比如订单状态、库存扣减)、能否接受失败后重试或提示用户刷新。
C# 用 Entity Framework 实现乐观锁:加 Version 字段 + ConcurrencyCheck
这是最常用也最稳妥的乐观锁落地方式。EF Core 会自动在 UPDATE 的 WHERE 子句中带上版本号校验,避免覆盖式更新。
- 实体类里加一个
[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 UPDATE(MySQL/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,否则一个卡住的事务可能拖垮整个连接池 - 混合使用常见但难维护:比如主流程用乐观锁,关键资金操作切悲观锁——这时要确保事务边界清晰,避免跨上下文锁升级失败
真正难的不是选锁,而是定义清楚“哪条数据谁在什么时候可能被谁改”。画一张状态流转图,标出所有并发修改点,再挨个决定用哪种锁——这比背概念管用十倍。










