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