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

.NET Core WebAPI重复提交致重复数据的服务端解决方案问询

解决.NET Core WebAPI并行请求导致重复数据的服务端方案

这个并发重复提交的问题确实很头疼——前端禁用按钮只能防君子,遇到快速点击或者恶意请求就失效了。除了数据库唯一约束,我给你几个实用的服务端解决方案,覆盖不同部署场景和业务需求:

1. 针对业务粒度的本地/分布式锁

如果你的服务是单实例部署,用SemaphoreSlim做细粒度锁是最快的方案——既不会锁住整个接口,又能保证同一类请求(比如相同MainGroup的创建请求)串行处理。

比如可以在服务类里维护一个按MainGroup分组的信号量字典:

// 用ConcurrentDictionary保证线程安全
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _groupLocks = new ConcurrentDictionary<string, SemaphoreSlim>();

public async Task<ReturnMessageMain> Create(CreateOrEditMainCategoryDto input) {
    var groupKey = input.MainGroup.Trim();
    // 为当前MainGroup获取或创建信号量,初始许可数设为1
    var semaphore = _groupLocks.GetOrAdd(groupKey, _ => new SemaphoreSlim(1, 1));
    
    try {
        await semaphore.WaitAsync(); // 等待获取锁
        
        // 原有的校验逻辑
        if (_mainCategoryRepository.GetAll().Any(x => x.MainGroup == groupKey)) {
            throw new UserFriendlyException("Main Category already exists");
        }
        
        // 插入数据库逻辑
        var mainCategory = ObjectMapper.Map<MainCategory>(input);
        int id = await _mainCategoryRepository.InsertAndGetIdAsync(mainCategory);
        
        return new ReturnMessageMain() {
            Id = id,
            Code = input.MainGroupCode,
            Message = "Saved Successfully"
        };
    } finally {
        semaphore.Release(); // 无论成功失败都释放锁
        // 可选:如果信号量回到初始状态,从字典移除避免内存占用
        if (semaphore.CurrentCount == 1) {
            _groupLocks.TryRemove(groupKey, out _);
        }
    }
}

如果是多实例部署,本地锁就失效了,这时候得用分布式锁——比如基于Redis的RedLock,或者数据库行级锁。核心思路是:用一个全局唯一的锁标识(比如MainGroup的值),让所有实例在处理前先竞争锁,拿到锁的才能执行后续逻辑。

2. 实现请求幂等性

幂等性的核心是:同一个请求无论调用多少次,结果都是一样的。适合需要重复请求返回相同响应的场景,比如表单提交。

具体步骤:

  • 前端每次提交时生成一个唯一的幂等键(比如UUID),放在请求头(比如Idempotency-Key)里
  • 后端先校验这个幂等键是否已经处理过,处理过就直接返回之前的结果;没处理过就执行逻辑,并把结果缓存起来

示例代码:

public async Task<ReturnMessageMain> Create(CreateOrEditMainCategoryDto input, 
    [FromHeader(Name = "Idempotency-Key")] string idempotencyKey) {
    
    if (string.IsNullOrEmpty(idempotencyKey)) {
        throw new UserFriendlyException("请提供幂等标识");
    }
    
    var cacheKey = $"Idempotency:MainCategory:{idempotencyKey}";
    // 从缓存(比如Redis)读取已有的处理结果
    var cachedResult = await _cacheService.GetAsync<ReturnMessageMain>(cacheKey);
    
    if (cachedResult != null) {
        return cachedResult; // 直接返回之前的结果,不重复执行逻辑
    }
    
    try {
        // 原有的校验和插入逻辑
        var groupKey = input.MainGroup.Trim();
        if (_mainCategoryRepository.GetAll().Any(x => x.MainGroup == groupKey)) {
            throw new UserFriendlyException("Main Category already exists");
        }
        
        var mainCategory = ObjectMapper.Map<MainCategory>(input);
        int id = await _mainCategoryRepository.InsertAndGetIdAsync(mainCategory);
        
        var successResult = new ReturnMessageMain() {
            Id = id,
            Code = input.MainGroupCode,
            Message = "Saved Successfully"
        };
        
        // 缓存结果,比如保留5分钟
        await _cacheService.SetAsync(cacheKey, successResult, TimeSpan.FromMinutes(5));
        return successResult;
    } catch (UserFriendlyException ex) {
        // 异常结果也缓存,避免重复抛出相同错误
        var errorResult = new ReturnMessageMain() {
            Id = 0,
            Message = ex.Message
        };
        await _cacheService.SetAsync(cacheKey, errorResult, TimeSpan.FromMinutes(5));
        throw;
    }
}

3. 数据库悲观锁+事务

把校验和插入操作放到同一个数据库事务里,并用悲观锁锁住查询范围,避免并行请求的间隙问题。

示例代码(以EF Core为例):

public async Task<ReturnMessageMain> Create(CreateOrEditMainCategoryDto input) {
    var dbContext = _mainCategoryRepository.GetDbContext();
    using (var transaction = await dbContext.Database.BeginTransactionAsync()) {
        try {
            var groupKey = input.MainGroup.Trim();
            
            // 查询时加UPDLOCK和HOLDLOCK,锁住符合条件的行范围
            var exists = await dbContext.MainCategories
                .FromSqlRaw(@"SELECT * FROM MainCategories WITH (UPDLOCK, HOLDLOCK) WHERE MainGroup = {0}", groupKey)
                .AnyAsync();
            
            if (exists) {
                throw new UserFriendlyException("Main Category already exists");
            }
            
            var mainCategory = ObjectMapper.Map<MainCategory>(input);
            int id = await _mainCategoryRepository.InsertAndGetIdAsync(mainCategory);
            
            await transaction.CommitAsync();
            
            return new ReturnMessageMain() {
                Id = id,
                Code = input.MainGroupCode,
                Message = "Saved Successfully"
            };
        } catch {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

这种方式依赖数据库的锁机制,会阻塞并行请求,适合对性能要求不是极致苛刻的场景。

最后提醒

无论用哪种方案,数据库唯一约束一定要保留——这是最后一道防线,避免因为锁失效、缓存异常等极端情况导致的数据重复。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 08:51:30