From fa0b93446f2c0372bcbdc02cc7c64fae5edec7a3 Mon Sep 17 00:00:00 2001 From: ssss <111@qq.com> Date: Fri, 3 Jul 2026 14:27:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(concurrency):=20=E4=BF=AE=E5=A4=8D=E7=BB=B4?= =?UTF-8?q?=E5=BA=A6=E5=92=8C=E5=9C=BA=E6=99=AF=E5=88=9B=E5=BB=BA=E7=9A=84?= =?UTF-8?q?=E7=AB=9E=E6=80=81=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/DimensionService.cs | 136 ++++++++++++------ .../Services/ScenarioService.cs | 106 +++++++++++--- 2 files changed, 181 insertions(+), 61 deletions(-) diff --git a/src/WCTDataMiner.Core/Services/DimensionService.cs b/src/WCTDataMiner.Core/Services/DimensionService.cs index 5bdf9d9..9e7cc4f 100644 --- a/src/WCTDataMiner.Core/Services/DimensionService.cs +++ b/src/WCTDataMiner.Core/Services/DimensionService.cs @@ -1,82 +1,138 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using WCTDataMiner.Core.Data; using WCTDataMiner.Core.Models; namespace WCTDataMiner.Core.Services; /// -/// 维度表管理服务 - 负责维度表的CRUD操作 +/// 维度表管理服务 - 负责维度表的CRUD操作(并发安全版本) /// public class DimensionService { private readonly WctMinerDbContext _context; + private readonly ILogger _logger; - public DimensionService(WctMinerDbContext context) + public DimensionService(WctMinerDbContext context, ILogger logger) { _context = context; + _logger = logger; } /// - /// 获取或创建TX面板维度 + /// 获取或创建TX面板维度(并发安全) /// public async Task GetOrCreateTxPanelAsync(string name) { - var existing = await _context.TxPanels - .FirstOrDefaultAsync(p => p.Name == name); - - if (existing != null) return existing; - - var newPanel = new TxPanel { Name = name }; - _context.TxPanels.Add(newPanel); - await _context.SaveChangesAsync(); - return newPanel; + return await GetOrCreateDimensionAsync( + () => _context.TxPanels.AsNoTracking().FirstOrDefaultAsync(p => p.Name == name), + () => new TxPanel { Name = name }, + entity => _context.TxPanels.Add(entity), + name, + "TX Panel" + ); } /// - /// 获取或创建TX硬件版本维度 + /// 获取或创建TX硬件版本维度(并发安全) /// public async Task GetOrCreateTxHardwareAsync(string version) { - var existing = await _context.TxHardwares - .FirstOrDefaultAsync(h => h.Version == version); - - if (existing != null) return existing; - - var newHardware = new TxHardware { Version = version }; - _context.TxHardwares.Add(newHardware); - await _context.SaveChangesAsync(); - return newHardware; + return await GetOrCreateDimensionAsync( + () => _context.TxHardwares.AsNoTracking().FirstOrDefaultAsync(h => h.Version == version), + () => new TxHardware { Version = version }, + entity => _context.TxHardwares.Add(entity), + version, + "TX Hardware" + ); } /// - /// 获取或创建TX软件版本维度 + /// 获取或创建TX软件版本维度(并发安全) /// public async Task GetOrCreateTxSoftwareAsync(string version) { - var existing = await _context.TxSoftwares - .FirstOrDefaultAsync(s => s.Version == version); - - if (existing != null) return existing; - - var newSoftware = new TxSoftware { Version = version }; - _context.TxSoftwares.Add(newSoftware); - await _context.SaveChangesAsync(); - return newSoftware; + return await GetOrCreateDimensionAsync( + () => _context.TxSoftwares.AsNoTracking().FirstOrDefaultAsync(s => s.Version == version), + () => new TxSoftware { Version = version }, + entity => _context.TxSoftwares.Add(entity), + version, + "TX Software" + ); } /// - /// 获取或创建RX类型维度 + /// 获取或创建RX类型维度(并发安全) /// public async Task GetOrCreateRxTypeAsync(string name) { - var existing = await _context.RxTypes - .FirstOrDefaultAsync(r => r.Name == name); + return await GetOrCreateDimensionAsync( + () => _context.RxTypes.AsNoTracking().FirstOrDefaultAsync(r => r.Name == name), + () => new RxType { Name = name }, + entity => _context.RxTypes.Add(entity), + name, + "RX Type" + ); + } - if (existing != null) return existing; + /// + /// 通用的并发安全 GetOrCreate 模式 + /// + private async Task GetOrCreateDimensionAsync( + Func> findExisting, + Func createNew, + Action addToContext, + string identifier, + string dimensionName) where T : class + { + const int maxRetries = 3; - var newRxType = new RxType { Name = name }; - _context.RxTypes.Add(newRxType); - await _context.SaveChangesAsync(); - return newRxType; + for (int attempt = 0; attempt < maxRetries; attempt++) + { + // 使用 AsNoTracking 避免 ChangeTracker 缓存 + var existing = await findExisting(); + + if (existing != null) return existing; + + var newEntity = createNew(); + addToContext(newEntity); + + try + { + await _context.SaveChangesAsync(); + _logger.LogDebug("创建新 {DimensionName} 维度: {Identifier}", dimensionName, identifier); + return newEntity; + } + catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex)) + { + // Detach 实体以避免 ChangeTracker 问题 + _context.Entry(newEntity).State = EntityState.Detached; + _logger.LogDebug("{DimensionName} 并发创建冲突,重试: {Identifier}", dimensionName, identifier); + + if (attempt < maxRetries - 1) + { + // 随机退避,减少并发冲突 + await Task.Delay(Random.Shared.Next(5, 20)); + } + } + } + + // 最后一次尝试:必然存在(唯一约束已生效) + var finalEntity = await findExisting(); + if (finalEntity == null) + { + // 极端情况:重试全部失败但数据不存在,抛出异常 + throw new InvalidOperationException($"无法获取或创建 {dimensionName} 维度: {identifier}"); + } + return finalEntity; + } + + /// + /// 检测 SQLite 唯一约束违反错误 + /// + private static bool IsUniqueConstraintViolation(DbUpdateException ex) + { + return ex.InnerException is Microsoft.Data.Sqlite.SqliteException sqliteEx + && sqliteEx.SqliteErrorCode == 19; // SQLITE_CONSTRAINT } } \ No newline at end of file diff --git a/src/WCTDataMiner.Core/Services/ScenarioService.cs b/src/WCTDataMiner.Core/Services/ScenarioService.cs index ee8fc98..c1b4b10 100644 --- a/src/WCTDataMiner.Core/Services/ScenarioService.cs +++ b/src/WCTDataMiner.Core/Services/ScenarioService.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using WCTDataMiner.Core.Data; using WCTDataMiner.Core.Models; using WCTDataMiner.Core.Parsers; @@ -6,32 +7,98 @@ using WCTDataMiner.Core.Parsers; namespace WCTDataMiner.Core.Services; /// -/// 测试场景服务 - 负责测试场景的创建和查询 +/// 测试场景服务 - 负责测试场景的创建和查询(并发安全版本) /// public class ScenarioService { private readonly WctMinerDbContext _context; private readonly DimensionService _dimensionService; + private readonly ILogger _logger; - public ScenarioService(WctMinerDbContext context, DimensionService dimensionService) + public ScenarioService(WctMinerDbContext context, DimensionService dimensionService, ILogger logger) { _context = context; _dimensionService = dimensionService; + _logger = logger; } /// - /// 根据场景信息获取或创建测试场景 + /// 根据场景信息获取或创建测试场景(并发安全) /// public async Task GetOrCreateScenarioAsync(ScenarioInfo info) { + _logger.LogDebug("查找/创建场景: TxPanel={TxPanel}, TxHardware={TxHardware}, TxSoftware={TxSoftware}, RxType={RxType}", + info.TxPanel, info.TxHardware, info.TxSoftware, info.RxType); + // 1. 获取或创建各维度 var txPanel = await _dimensionService.GetOrCreateTxPanelAsync(info.TxPanel); var txHardware = await _dimensionService.GetOrCreateTxHardwareAsync(info.TxHardware); var txSoftware = await _dimensionService.GetOrCreateTxSoftwareAsync(info.TxSoftware); var rxType = await _dimensionService.GetOrCreateRxTypeAsync(info.RxType); - // 2. 检查场景是否已存在(利用唯一约束) - var existing = await _context.TestScenarios + // 2. 并发安全的场景创建 + const int maxRetries = 3; + + for (int attempt = 0; attempt < maxRetries; attempt++) + { + // 使用 AsNoTracking 避免 ChangeTracker 缓存 + var existing = await _context.TestScenarios + .AsNoTracking() + .FirstOrDefaultAsync(s => + s.TxPanelId == txPanel.Id && + s.TxHardwareId == txHardware.Id && + s.TxSoftwareId == txSoftware.Id && + s.RxTypeId == rxType.Id && + s.TestPurpose == info.TestPurpose && + s.TestDate == info.TestDate && + s.TestSequence == info.TestSequence); + + if (existing != null) + { + _logger.LogDebug("场景已存在: {ScenarioId}", existing.Id); + return existing; + } + + // 3. 创建新场景 + var scenario = new TestScenario + { + TxPanelId = txPanel.Id, + TxHardwareId = txHardware.Id, + TxSoftwareId = txSoftware.Id, + RxTypeId = rxType.Id, + TestPurpose = info.TestPurpose, + TestDate = info.TestDate, + TestSequence = info.TestSequence + }; + + _context.TestScenarios.Add(scenario); + + try + { + await _context.SaveChangesAsync(); + + _logger.LogInformation("创建新测试场景: {ScenarioId}, TxPanel={TxPanel}, TxHardware={TxHardware}, RxType={RxType}, TestDate={TestDate}", + scenario.Id, info.TxPanel, info.TxHardware, info.RxType, info.TestDate); + + return scenario; + } + catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex)) + { + // Detach 实体以避免 ChangeTracker 问题 + _context.Entry(scenario).State = EntityState.Detached; + _logger.LogDebug("场景并发创建冲突,重试: TxPanel={TxPanel}", info.TxPanel); + + if (attempt < maxRetries - 1) + { + // 随机退避,减少并发冲突 + await Task.Delay(Random.Shared.Next(5, 20)); + } + } + } + + // 最后一次尝试:必然存在(唯一约束已生效) + var finalScenario = await _context.TestScenarios + .AsNoTracking() .FirstOrDefaultAsync(s => s.TxPanelId == txPanel.Id && s.TxHardwareId == txHardware.Id && @@ -41,24 +108,12 @@ public class ScenarioService s.TestDate == info.TestDate && s.TestSequence == info.TestSequence); - if (existing != null) return existing; - - // 3. 创建新场景 - var scenario = new TestScenario + if (finalScenario == null) { - TxPanelId = txPanel.Id, - TxHardwareId = txHardware.Id, - TxSoftwareId = txSoftware.Id, - RxTypeId = rxType.Id, - TestPurpose = info.TestPurpose, - TestDate = info.TestDate, - TestSequence = info.TestSequence - }; + throw new InvalidOperationException($"无法获取或创建场景: {info.TxPanel}/{info.TxHardware}/{info.RxType}"); + } - _context.TestScenarios.Add(scenario); - await _context.SaveChangesAsync(); - - return scenario; + return finalScenario; } /// @@ -73,4 +128,13 @@ public class ScenarioService .Include(s => s.RxType) .FirstOrDefaultAsync(s => s.Id == scenarioId); } + + /// + /// 检测 SQLite 唯一约束违反错误 + /// + private static bool IsUniqueConstraintViolation(DbUpdateException ex) + { + return ex.InnerException is Microsoft.Data.Sqlite.SqliteException sqliteEx + && sqliteEx.SqliteErrorCode == 19; // SQLITE_CONSTRAINT + } } \ No newline at end of file