导读:本期聚焦于布兰登创作的《c#数据库乐观锁和悲观锁哪个好?如何根据场景选择》,敬请观看详情。并发场景下的数据一致性问题是每个后端开发者绕不开的坎。当两个用户同时修改同一条库存记录,超卖问题就随之而来。解决这类问题的核心手段就是锁策略,其中乐观锁和悲观锁是最常见的两种思路。本文从两种锁的底层原理讲起,用c#配合SQL Server和EF Core给出可直接运行的代码示例,对比它们在吞吐量、响应时间和死锁风险上的差异,并结合库存扣减、账户转账、订单审核等典型业务场景,给出明确的选择建议。读完本文你将掌握如何根据冲突概率、并发量和业务容忍度挑选合适的锁策略,避免常见的并发踩坑。

在一个电商系统中,库存只剩最后一件,两个用户几乎同时点击了购买按钮。如果没有并发控制,很可能出现超卖:两个请求都读到库存为1,各自扣减后写入0,实际却卖出了两件。解决这类问题的核心技术就是乐观锁和悲观锁。这两种思路在实现方式、性能表现和适用场景上差异很大,很多c#开发者只知道概念,却不知道在实际项目中该怎么选。本文把两者的原理、代码实现和选型思路一次讲透。

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

乐观锁和悲观锁的底层原理

首先要明确一点:乐观锁和悲观锁是一种并发控制的思想,而不是数据库中某个具体的锁对象。悲观锁指的是在读取数据时就认为冲突一定会发生,于是直接对数据加锁,其他事务在锁释放前无法修改这条数据。在SQL Server中通常通过SELECT ... WITH (UPDLOCK, ROWLOCK)实现,MySQL中则是SELECT ... FOR UPDATE。它的特点是先加锁、后操作,整个过程中数据被独占。

乐观锁则相反,它读取数据时不加任何锁,只在提交更新的一瞬间检查数据是否被别人改过。最经典的实现是版本号机制:表中加一个Version字段,每次更新时版本号加一,UPDATE语句的WHERE条件里带上读出来的旧版本号。如果受影响行数为0,说明数据已经被其他事务修改,本次更新失败,需要业务层决定是重试还是报错。除了版本号,也可以用时间戳或者直接比对整行字段值(类似EF Core的并发令牌)。

两者的本质区别在于对待冲突的态度:悲观锁假设冲突频繁,用加锁换取确定性;乐观锁假设冲突稀少,用重试机制换取更高的吞吐。这个假设决定了它们的适用边界,也是后面选型的核心依据。

c#中实现两种锁的完整代码

先看悲观锁的实现。假设有一个商品库存表,使用ADO.NET配合SQL Server的UPDLOCK提示,在事务内锁定目标行:

public bool DeductStockPessimistic(SqlConnection conn, int productId, int quantity)
{
    conn.Open();
    using var transaction = conn.BeginTransaction();
    try
    {
        // UPDLOCK 让其他事务无法同时修改这一行
        string sql = @"SELECT Stock FROM Products WITH (UPDLOCK, ROWLOCK)
                       WHERE ProductId = @ProductId";
        int stock;
        using (var cmd = new SqlCommand(sql, conn, transaction))
        {
            cmd.Parameters.AddWithValue("@ProductId", productId);
            stock = Convert.ToInt32(cmd.ExecuteScalar());
        }
        if (stock < quantity)
        {
            transaction.Rollback();
            return false; // 库存不足
        }
        string updateSql = @"UPDATE Products SET Stock = Stock - @Qty
                             WHERE ProductId = @ProductId";
        using (var cmd = new SqlCommand(updateSql, conn, transaction))
        {
            cmd.Parameters.AddWithValue("@Qty", quantity);
            cmd.Parameters.AddWithValue("@ProductId", productId);
            cmd.ExecuteNonQuery();
        }
        transaction.Commit();
        return true;
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}

这段代码的关键在于UPDLOCK:从SELECT开始该行就被锁定,直到事务提交,其他事务的同类查询会阻塞等待。也就是说,检查库存和扣减库存两步操作是原子的,不可能出现超卖。

再看乐观锁的版本号实现:

public bool DeductStockOptimistic(SqlConnection conn, int productId, int quantity)
{
    conn.Open();
    // 第一步:不加锁地读出库存和版本号
    int stock, version;
    string readSql = @"SELECT Stock, Version FROM Products
                       WHERE ProductId = @ProductId";
    using (var cmd = new SqlCommand(readSql, conn))
    {
        cmd.Parameters.AddWithValue("@ProductId", productId);
        using var reader = cmd.ExecuteReader();
        reader.Read();
        stock = reader.GetInt32(0);
        version = reader.GetInt32(1);
    }
    if (stock < quantity) return false;
    // 第二步:带上旧版本号更新,版本号不匹配则失败
    string updateSql = @"UPDATE Products
                         SET Stock = Stock - @Qty, Version = Version + 1
                         WHERE ProductId = @ProductId AND Version = @Version";
    using (var cmd = new SqlCommand(updateSql, conn))
    {
        cmd.Parameters.AddWithValue("@Qty", quantity);
        cmd.Parameters.AddWithValue("@ProductId", productId);
        cmd.Parameters.AddWithValue("@Version", version);
        int affected = cmd.ExecuteNonQuery();
        return affected > 0; // 0表示并发冲突,需要重试
    }
}

如果使用EF Core,乐观锁的写法更简洁,只需把版本字段配置为并发令牌:

// 实体类
public class Product
{
    public int ProductId { get; set; }
    public int Stock { get; set; }
    [Timestamp]
    public byte[] RowVersion { get; set; }
}

// 更新时捕获并发冲突
try
{
    product.Stock -= quantity;
    await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
    // 数据已被其他人修改,提示用户刷新重试
    return Conflict("数据已被其他用户修改,请刷新后重试");
}

注意EF Core抛出的DbUpdateConcurrencyException就是乐观锁冲突的信号,此时数据库中的值已经和你内存中的实体不一致,直接覆盖保存会产生脏数据,正确做法是把冲突抛给上层处理。

性能对比与场景选择建议

从性能角度看,悲观锁在冲突频繁的场景下表现更好,因为它避免了大量失败重试带来的CPU和连接开销;而乐观锁在冲突稀少时几乎零开销,读取阶段完全不加锁,数据库并发吞吐量最高。但如果冲突率高,乐观锁会导致大量更新失败和重试,反而拖垮系统;悲观锁在高并发下则容易出现锁等待、连接池耗尽甚至死锁。

具体怎么选,可以参考以下几个判断维度。第一看冲突概率:热点数据(如秒杀商品、热门直播间)冲突极高,用悲观锁或者直接上分布式锁更稳妥;普通业务数据(如用户资料、订单备注)冲突极低,乐观锁足够。第二看业务能否接受重试:乐观锁失败后需要用户或程序重试,如果是后台任务可以自动重试,如果是前端表单提交则最好给出友好提示。第三看事务时长:悲观锁持有锁的时间等于事务时长,事务里千万别夹带RPC调用、发短信这类慢操作,否则锁会一直不释放。

实际项目中两者经常组合使用。比如订单审核流程用乐观锁版本号防止审核和修改互踩,而资金转账这类强一致性要求极高的操作用悲观锁串行化处理。还有一个常见的折中方案是条件更新,即UPDATE Products SET Stock = Stock - @Qty WHERE Stock >= @Qty,一行SQL原子完成检查和扣减,本质上是数据库层面的乐观控制,既没有版本号字段,也不需要显式加锁,在库存扣减场景中往往是首选。

总结一句:并发量小、冲突少、业务可重试,选乐观锁;并发集中、冲突频繁、一致性要求强,选悲观锁。拿不准的时候,先用条件更新这种简单方案兜底,等监控数据证明确实存在冲突热点,再升级到显式锁策略,这样既不过度设计,也留足了演进空间。

c#乐观锁悲观锁并发控制修改时间:2026-09-05 09:42:45

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260905/50826.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。