You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Entity Framework Core下SQL Server特定表查询阻塞问题求助

问题分析与解决方案

你的问题核心是高并发场景下的行级锁竞争:35个应用实例同时针对OrderAtDay表中同一日期的行执行"查询存在→新增/更新"的两步操作,SQL Server的默认锁机制会导致大量会话被阻塞——这也是只有这张表出问题的原因,它是所有实例的共享热点数据。

以下是针对性的解决/规避方案:

1. 用原子化操作替代"先查后改"

把检查行存在和更新/插入合并成一个原子SQL操作,彻底消除两步操作之间的锁间隙。

  • EF Core 7.0+ 原生支持Upsert:
    await _dbContext.OrderAtDay
        .Upsert(new OrderAtDay { Date = targetDate, CurrentAmountOfOrders = 1 })
        .WhenMatched(o => new OrderAtDay { CurrentAmountOfOrders = o.CurrentAmountOfOrders + 1 })
        .ExecuteAsync();
    
  • 低版本EF Core或原生SQL:使用SQL Server的MERGE语句
    MERGE INTO OrderAtDay AS target
    USING (SELECT @TargetDate AS Date) AS source
    ON target.Date = source.Date
    WHEN MATCHED THEN
        UPDATE SET CurrentAmountOfOrders = CurrentAmountOfOrders + 1
    WHEN NOT MATCHED THEN
        INSERT (Date, CurrentAmountOfOrders)
        VALUES (@TargetDate, 1);
    
    在EF中通过FromSqlRaw执行该语句,确保操作是原子性的,锁持有时间最短。

2. 优化事务隔离与锁策略

  • 启用SNAPSHOT隔离级别:既然已经开了READ_COMMITTED_SNAPSHOT,可以进一步将事务隔离级别设为SNAPSHOT,让读操作基于快照不阻塞写,写操作也不阻塞读。
    在EF Core中设置:
    await using var transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.Snapshot);
    try
    {
        // 你的订单处理逻辑
        await _dbContext.SaveChangesAsync();
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
    
  • 缩短事务时长:确保订单处理逻辑中,数据库操作只包含必要的OrderAtDay更新和订单插入,避免在事务中加入外部API调用、文件IO等耗时操作,减少锁持有时间。

3. 乐观并发控制替代悲观锁

用版本号冲突处理并发,避免长时间持有行锁:

  1. 给OrderAtDay实体添加并发令牌:
    public class OrderAtDay
    {
        public int CurrentAmountOfOrders { get; set; }
        public DateTime Date { get; set; }
        public byte[] RowVersion { get; set; } // 并发令牌
    }
    
  2. 在EF配置中标记为行版本:
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<OrderAtDay>()
            .Property(o => o.RowVersion)
            .IsRowVersion(); // SQL Server自动生成并维护版本号
    }
    
  3. 处理并发冲突并重试:
    int retryCount = 3;
    while (retryCount > 0)
    {
        try
        {
            var orderAtDay = await _dbContext.OrderAtDay.FirstOrDefaultAsync(o => o.Date == targetDate);
            if (orderAtDay == null)
            {
                _dbContext.OrderAtDay.Add(new OrderAtDay { Date = targetDate, CurrentAmountOfOrders = 1 });
            }
            else
            {
                orderAtDay.CurrentAmountOfOrders++;
            }
            await _dbContext.SaveChangesAsync();
            break;
        }
        catch (DbUpdateConcurrencyException)
        {
            _dbContext.ChangeTracker.Clear(); // 清除上下文缓存,重新读取最新数据
            retryCount--;
            if (retryCount == 0) throw;
        }
    }
    

4. 用分布式缓存替代数据库行锁

把每日订单计数器转移到Redis等分布式缓存中,彻底避开数据库锁竞争:

  • 逻辑示例:
    // 构造当日计数器的Redis Key
    var redisKey = $"OrderCounter:{DateTime.Today:yyyyMMdd}";
    // 原子递增,返回递增后的值(即当日订单编号)
    var dailyOrderNumber = await _redisDb.StringIncrementAsync(redisKey);
    // 设置Key过期时间,避免占用缓存
    await _redisDb.KeyExpireAsync(redisKey, TimeSpan.FromDays(1));
    
    // 后续将dailyOrderNumber写入订单表即可
    
    这种方式下,所有应用实例都通过Redis的原子操作获取编号,完全不需要操作OrderAtDay表,从根源解决锁问题。

5. 数据库层面优化

  • 添加唯一索引:给OrderAtDay.Date字段添加唯一索引,确保查询和更新时能快速定位行,避免全表扫描导致的锁升级:
    CREATE UNIQUE NONCLUSTERED INDEX IX_OrderAtDay_Date ON OrderAtDay(Date);
    
  • 设置锁超时:避免会话无限期阻塞,在EF中设置命令超时:
    _dbContext.Database.SetCommandTimeout(30); // 30秒超时
    
    或在SQL Server中全局设置:
    SET LOCK_TIMEOUT 30000; -- 单位毫秒,30秒
    

排查辅助

如果问题仍存在,可以用以下SQL查看锁详情,确认锁类型和资源:

SELECT 
    request_session_id AS SessionId,
    resource_type AS LockType,
    resource_description AS LockResource,
    request_mode AS LockMode,
    request_status AS LockStatus
FROM sys.dm_tran_locks
WHERE resource_database_id = DB_ID();

内容的提问来源于stack exchange,提问作者THEoneANDonly

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 12:06:15