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

如何用Entity Framework防止客户端并发插入重复记录?

如何避免API并发调用时的重复数据插入?

哥们,你当前写的IsExistIn10Seconds逻辑在4个客户端并发调用的场景下根本挡不住重复插入,核心问题在于「检查-插入」不是原子操作:四个请求可能同时跑到查询步骤,这时候数据库里还没有任何记录,所以都能通过检查,接着各自执行插入,最后还是会出现4条重复数据。

为什么当前逻辑失效?

这种先查询再判断的模式,在高并发下会出现竞态条件——查询和插入之间有时间差,其他请求完全可以在这个空隙里完成同样的查询,然后一起插入数据。

更优的实现方案(按可靠性排序)

1. 数据库层面的联合唯一约束(最推荐)

这是最底层、最可靠的保障,直接让数据库帮你挡住重复数据。你可以给Histories表创建一个联合唯一索引,把Number和「截断到10秒窗口的EntryDateTime」绑定在一起:

  • 比如在SQL Server里,你可以创建这样的索引:

    CREATE UNIQUE NONCLUSTERED INDEX IX_Histories_Number_EntryTimeWindow
    ON Histories (Number, DATEADD(second, DATEDIFF(second, 0, EntryDateTime)/10*10, 0))
    

    这个索引会把EntryDateTime截断到每10秒的起始点(比如10:00:00、10:00:10、10:00:20...),同一个号码在同一个10秒窗口里只能插入一条数据。

  • 代码层面只需捕获唯一约束冲突的异常,然后返回「已存在」的结果即可:

    public async Task<bool> TryInsertHistory(string number, DateTime entryTime)
    {
        try
        {
            DbContext.Histories.Add(new History { Number = number, EntryDateTime = entryTime });
            await DbContext.SaveChangesAsync();
            return true;
        }
        catch (DbUpdateException ex)
        {
            // 检查是否是唯一约束冲突(不同数据库异常类型可能有差异)
            if (ex.InnerException is SqlException sqlEx && sqlEx.Number == 2601)
            {
                return false; // 表示重复插入,已存在
            }
            throw;
        }
    }
    

2. 分布式锁(跨服务/多实例场景)

如果你的API是多实例部署的,单数据库锁可能不够用,这时候可以用Redis这类分布式锁,以Number为锁键,锁的过期时间设为10秒:

public async Task<bool> TryInsertHistory(string number, DateTime entryTime)
{
    // 假设_redisLockService是你封装的Redis锁服务
    using var redisLock = await _redisLockService.LockAsync($"history_lock:{number}", TimeSpan.FromSeconds(10));
    if (!redisLock.IsAcquired)
    {
        return false; // 并发请求,直接返回已存在或操作中
    }

    // 这里再执行检查和插入(加锁后竞态条件就不存在了)
    var existing = await DbContext.Histories
        .Where(o => o.Number == number && o.EntryDateTime >= DateTime.Now.AddSeconds(-10))
        .FirstOrDefaultAsync();
    if (existing == null)
    {
        DbContext.Histories.Add(new History { Number = number, EntryDateTime = entryTime });
        await DbContext.SaveChangesAsync();
        return true;
    }
    return false;
}

3. 数据库悲观锁(单实例场景)

如果你的API是单实例部署,也可以用EF Core的悲观锁,在查询时锁住符合条件的行,阻止其他请求的查询操作:

public async Task<bool> TryInsertHistory(string number, DateTime entryTime)
{
    using var transaction = await DbContext.Database.BeginTransactionAsync();
    try
    {
        // 加上悲观写锁,锁住查询到的行(如果没有行,就锁住对应的范围)
        var existing = await DbContext.Histories
            .Where(o => o.Number == number && o.EntryDateTime >= DateTime.Now.AddSeconds(-10))
            .FirstOrDefaultAsync().WithLock(LockMode.PessimisticWrite);
        
        if (existing == null)
        {
            DbContext.Histories.Add(new History { Number = number, EntryDateTime = entryTime });
            await DbContext.SaveChangesAsync();
            await transaction.CommitAsync();
            return true;
        }
        await transaction.RollbackAsync();
        return false;
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

总结

  • 优先用数据库唯一约束,它是最可靠的,即使应用层出问题,数据库也能守住最后一道防线;
  • 多实例部署选分布式锁;
  • 单实例场景可以用悲观锁,但性能上不如唯一约束。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 07:40:14