当前位置:首页 > 技术 > 正文内容

C# 数据库并发控制方案:从乐观锁到 Entity Framework 实践

访客 技术 2026年10月12日 1

并发问题的本质

在多用户同时访问系统的环境下,当多个进程或线程尝试同时修改同一份数据时,就会产生并发问题。如果缺乏有效的控制机制,可能会导致数据不一致,如"丢失更新"、"脏读"或"不可重复读"。

针对并发的处理策略通常分为两大类:悲观并发控制(Pessimistic Concurrency)和乐观并发控制(Optimistic Concurrency)。

1. 悲观并发控制与乐观并发控制

悲观锁假设冲突频繁发生。在数据被读取时即进行锁定,直到事务完成。在 C# 中,本地内存级别的悲观锁可以通过 lock、Monitor 或 Mutex 实现;在数据库层面,通常通过 SELECT ... WITH (UPDLOCK) 等语法实现。其缺点是锁定的资源时间较长,在高并发场景下会导致系统吞吐量大幅下降。

乐观锁则假设冲突较少发生。它在读取数据时不加锁,而是在提交更新时检查数据自读取以来是否被其他用户修改。这种方式通常依赖于"版本号"或"时间戳"机制,具有更好的扩展性和性能。

2. 典型并发异常场景模拟

假设我们有一个简单的库存管理系统,包含库存表 Stock 和操作日志表 StockLog。以下是一个没有并发控制的减少库存操作示例:

public void UpdateInventory(int skuId)
{
    using (var db = new AppDbContext())
    {
        using (var trans = db.Database.BeginTransaction())
        {
            // 1. 获取当前库存
            var stockItem = db.Stocks.FirstOrDefault(s => s.SkuId == skuId);
            
            if (stockItem != null && stockItem.Count > 0)
            {
                // 2. 模拟耗时操作,增加并发冲突概率
                Thread.Sleep(500);

                // 3. 减少库存
                stockItem.Count -= 1;
                db.SaveChanges();

                // 4. 记录日志
                db.StockLogs.Add(new StockLog { Message = "库存扣减成功" });
                db.SaveChanges();
                
                trans.Commit();
            }
        }
    }
}

当多个用户同时调用此方法时,由于多个线程可能读取到相同的初始库存值,最终会导致日志记录了多次,但库存仅减少了一次,产生了典型的"并发覆盖"错误。

3. 通用数据库级乐观锁方案

在数据库层面实现乐观锁,最常用的方法是引入一个 rowversion(SQL Server)或 timestamp 字段。每次行数据发生变化时,数据库会自动更新该字段的值。

我们可以通过存储过程来实现基于版本的安全更新:

CREATE PROCEDURE sp_SafeDecreaseStock
    @SkuId INT,
    @IsSuccess BIT OUTPUT
AS
BEGIN
    DECLARE @OldVersion ROWVERSION;
    DECLARE @CurrentCount INT;

    BEGIN TRAN
        -- 获取当前库存和版本号
        SELECT @CurrentCount = [Count], @OldVersion = [Version] 
        FROM Stocks WHERE SkuId = @SkuId;

        -- 仅当版本号匹配时才执行更新
        UPDATE Stocks 
        SET [Count] = @CurrentCount - 1 
        WHERE SkuId = @SkuId AND [Version] = @OldVersion;

        IF @@ROWCOUNT > 0
        BEGIN
            INSERT INTO StockLogs (Message) VALUES ('乐观锁更新成功');
            SET @IsSuccess = 1;
        END
        ELSE
        BEGIN
            SET @IsSuccess = 0;
        END
    COMMIT TRAN
END

在 C# 调用端,如果执行失败(即 @IsSuccess = 0),可以采取重试机制:

public void ExecuteWithRetry(int skuId)
{
    int maxRetry = 5;
    bool success = false;
    
    while (!success && maxRetry > 0)
    {
        success = CallStoredProcedure(skuId);
        if (!success) 
        {
            maxRetry--;
            Thread.Sleep(100); // 短暂延迟后重试
        }
    }
}

4. Entity Framework 中的并发控制

Entity Framework (EF) 提供了内置的乐观锁支持。通过配置实体属性的 ConcurrencyMode,可以轻松处理冲突。

4.1 配置并发令牌

在 EF 的 .edmx 模型设计器中,或使用 Fluent API,可以将某个字段(如 RowVersion)的 ConcurrencyMode 设置为 Fixed。对于 SQL Server,建议使用 byte[] 类型并映射为 rowversion 字段。

// Fluent API 配置示例
modelBuilder.Entity<Stock>()
    .Property(s => s.Version)
    .IsRowVersion(); 

4.2 捕获并发异常

当执行 SaveChanges() 时,如果数据库中的版本号已改变,EF 会抛出 DbUpdateConcurrencyException。我们可以捕获该异常并决定是"数据库获胜"还是"当前客户端获胜"。

public bool SaveStockChanges(Stock updatedItem)
{
    using (var db = new AppDbContext())
    {
        try
        {
            db.Entry(updatedItem).State = EntityState.Modified;
            db.SaveChanges();
            return true;
        }
        catch (DbUpdateConcurrencyException ex)
        {
            // 获取当前的数据库值
            var entry = ex.Entries.Single();
            var databaseValues = entry.GetDatabaseValues();

            if (databaseValues == null)
            {
                // 记录已被删除
                return false;
            }
            else
            {
                // 这里可以根据业务逻辑决定:重试、合并数据或放弃
                return false; 
            }
        }
    }
}

5. 总结与建议

在处理高并发数据交互时:

  • 若并发频率极高且业务逻辑复杂,考虑在数据库层面使用存储过程结合版本检查,以减少网络往返开销。
  • 若使用 EF 等 ORM 工具,务必配置 rowversion 字段作为并发令牌,并编写稳健的异常捕获与重试逻辑。
  • 对于极度敏感的财务数据,在必要时仍需结合悲观锁(如事务隔离级别调整)确保绝对的数据安全性。
返回列表

上一篇:深度神经网络结构演进与核心设计模式

没有最新的文章了...

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。