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

如何为供应商计数器表实现无性能损耗的行锁,解决重复编号问题

解决并发场景下供应商计数器重复的最佳方案

这个问题我之前在高并发交易系统里碰到过,核心问题是你的当前代码存在竞态条件——多个请求同时查询到同一个计数器值,各自加1后保存,自然会出现重复编号。下面给你几个经过生产环境验证的解决方案,从EF Core原生优化到数据库原子操作都有,你可以根据自己的技术栈和性能需求选择:

方案一:EF Core原生行级锁优化

通过SELECT ... FOR UPDATE锁定目标行,确保同一时间只有一个请求能修改该供应商的计数器,避免并发冲突。同时给ProviderId字段加唯一约束,防止多个请求同时创建同一供应商的计数器记录。

public async Task<int> GetNumeration(int id)
{
    // 锁定目标行,直到事务结束
    var providerNumerationDb = await _dbContextEf.providerNumeration
        .FromSqlRaw("SELECT * FROM ProviderNumeration WHERE ProviderId = {0} FOR UPDATE", id)
        .SingleOrDefaultAsync();

    if (providerNumerationDb == null)
    {
        // 处理并发创建的情况:如果唯一键冲突,说明已有请求创建了记录,重新查询
        try
        {
            providerNumerationDb = new ProviderNumeration { ProviderId = id, Number = 1 };
            await _dbContextEf.AddAsync(providerNumerationDb);
            await _dbContextEf.SaveChangesAsync();
        }
        catch (DbUpdateException ex)
        {
            // SQL Server唯一键冲突错误码是2627,其他数据库请对应调整
            if (ex.InnerException is SqlException sqlEx && sqlEx.Number == 2627)
            {
                providerNumerationDb = await _dbContextEf.providerNumeration
                    .FromSqlRaw("SELECT * FROM ProviderNumeration WHERE ProviderId = {0} FOR UPDATE", id)
                    .SingleAsync();
            }
            else
            {
                throw;
            }
        }
    }
    else
    {
        // 原子更新计数器,避免先查后改的竞态
        await _dbContextEf.providerNumeration
            .Where(x => x.ProviderId == id)
            .ExecuteUpdateAsync(s => s.SetProperty(x => x.Number, x => x.Number + 1));
        providerNumerationDb.Number += 1;
    }

    _dbContextEf.Entry(providerNumerationDb).State = EntityState.Detached;
    return providerNumerationDb.Number;
}

优势:基于EF Core改造,不需要大幅调整现有代码结构;行级锁粒度小,对其他供应商的计数器操作无影响。

注意:如果是MySQL数据库,FOR UPDATE需要在事务中生效,EF Core默认的SaveChangesAsync会自动开启事务,所以没问题。

方案二:数据库原子操作(推荐)

直接在数据库端完成「查询+更新/插入」的原子操作,不需要EF上下文的状态管理,性能最优,因为只需要一次数据库往返,且数据库原生保证原子性,不需要额外手动加锁。

你可以用存储过程,或者直接在EF中执行原生SQL:

方式1:使用MERGE语句(SQL Server适用)

public async Task<int> GetNumeration(int id)
{
    var outputParam = new SqlParameter("@NewNumber", SqlDbType.Int) 
    { 
        Direction = ParameterDirection.Output 
    };

    await _dbContextEf.Database.ExecuteSqlRawAsync(@"
        MERGE ProviderNumeration AS target
        USING (SELECT @ProviderId AS ProviderId) AS source
        ON target.ProviderId = source.ProviderId
        WHEN MATCHED THEN
            UPDATE SET target.Number = target.Number + 1
        WHEN NOT MATCHED THEN
            INSERT (ProviderId, Number) VALUES (source.ProviderId, 1)
        OUTPUT inserted.Number INTO @NewNumber;
    ", new SqlParameter("@ProviderId", id), outputParam);

    return (int)outputParam.Value;
}

方式2:UPDATE+INSERT组合(兼容多数数据库)

public async Task<int> GetNumeration(int id)
{
    var outputParam = new SqlParameter("@NewNumber", SqlDbType.Int) 
    { 
        Direction = ParameterDirection.Output 
    };

    await _dbContextEf.Database.ExecuteSqlRawAsync(@"
        DECLARE @NewNumber INT;

        -- 先尝试更新计数器
        UPDATE ProviderNumeration 
        SET @NewNumber = Number = Number + 1
        WHERE ProviderId = @ProviderId;

        -- 如果没有找到记录,插入新的计数器
        IF @@ROWCOUNT = 0
        BEGIN
            INSERT INTO ProviderNumeration (ProviderId, Number)
            VALUES (@ProviderId, 1);
            SET @NewNumber = 1;
        END

        SELECT @NewNumber;
    ", new SqlParameter("@ProviderId", id), outputParam);

    return (int)outputParam.Value;
}

优势:完全避免EF上下文的竞态问题,性能比方案一更优;数据库原生保证操作原子性,不需要处理并发创建的重试逻辑。

方案三:分布式锁(多实例部署场景)

如果你的应用是多实例部署,且数据库行级锁无法覆盖跨实例的并发(比如部分缓存场景),可以用分布式锁(如Redis锁、ZooKeeper锁)。但这个方案复杂度较高,只有在必须的情况下才推荐,因为会引入额外的依赖和故障点。

总结推荐

如果追求性能和可靠性,方案二是最优选择——数据库原生的原子操作既避免了竞态条件,又不需要额外的锁管理,性能损耗几乎为零。如果不想大幅修改现有EF代码结构,方案一也是可靠的选择,只要确保ProviderId的唯一约束生效。

内容的提问来源于stack exchange,提问作者Nicolás Sosa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 19:32:40