fix(concurrency): 修复维度和场景创建的竞态条件

This commit is contained in:
ssss
2026-07-03 14:27:59 +08:00
parent c9be8f4ed2
commit fa0b93446f
2 changed files with 181 additions and 61 deletions

View File

@@ -1,82 +1,138 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using WCTDataMiner.Core.Data; using WCTDataMiner.Core.Data;
using WCTDataMiner.Core.Models; using WCTDataMiner.Core.Models;
namespace WCTDataMiner.Core.Services; namespace WCTDataMiner.Core.Services;
/// <summary> /// <summary>
/// 维度表管理服务 - 负责维度表的CRUD操作 /// 维度表管理服务 - 负责维度表的CRUD操作(并发安全版本)
/// </summary> /// </summary>
public class DimensionService public class DimensionService
{ {
private readonly WctMinerDbContext _context; private readonly WctMinerDbContext _context;
private readonly ILogger<DimensionService> _logger;
public DimensionService(WctMinerDbContext context) public DimensionService(WctMinerDbContext context, ILogger<DimensionService> logger)
{ {
_context = context; _context = context;
_logger = logger;
} }
/// <summary> /// <summary>
/// 获取或创建TX面板维度 /// 获取或创建TX面板维度(并发安全)
/// </summary> /// </summary>
public async Task<TxPanel> GetOrCreateTxPanelAsync(string name) public async Task<TxPanel> GetOrCreateTxPanelAsync(string name)
{ {
var existing = await _context.TxPanels return await GetOrCreateDimensionAsync(
.FirstOrDefaultAsync(p => p.Name == name); () => _context.TxPanels.AsNoTracking().FirstOrDefaultAsync(p => p.Name == name),
() => new TxPanel { Name = name },
if (existing != null) return existing; entity => _context.TxPanels.Add(entity),
name,
var newPanel = new TxPanel { Name = name }; "TX Panel"
_context.TxPanels.Add(newPanel); );
await _context.SaveChangesAsync();
return newPanel;
} }
/// <summary> /// <summary>
/// 获取或创建TX硬件版本维度 /// 获取或创建TX硬件版本维度(并发安全)
/// </summary> /// </summary>
public async Task<TxHardware> GetOrCreateTxHardwareAsync(string version) public async Task<TxHardware> GetOrCreateTxHardwareAsync(string version)
{ {
var existing = await _context.TxHardwares return await GetOrCreateDimensionAsync(
.FirstOrDefaultAsync(h => h.Version == version); () => _context.TxHardwares.AsNoTracking().FirstOrDefaultAsync(h => h.Version == version),
() => new TxHardware { Version = version },
if (existing != null) return existing; entity => _context.TxHardwares.Add(entity),
version,
var newHardware = new TxHardware { Version = version }; "TX Hardware"
_context.TxHardwares.Add(newHardware); );
await _context.SaveChangesAsync();
return newHardware;
} }
/// <summary> /// <summary>
/// 获取或创建TX软件版本维度 /// 获取或创建TX软件版本维度(并发安全)
/// </summary> /// </summary>
public async Task<TxSoftware> GetOrCreateTxSoftwareAsync(string version) public async Task<TxSoftware> GetOrCreateTxSoftwareAsync(string version)
{ {
var existing = await _context.TxSoftwares return await GetOrCreateDimensionAsync(
.FirstOrDefaultAsync(s => s.Version == version); () => _context.TxSoftwares.AsNoTracking().FirstOrDefaultAsync(s => s.Version == version),
() => new TxSoftware { Version = version },
if (existing != null) return existing; entity => _context.TxSoftwares.Add(entity),
version,
var newSoftware = new TxSoftware { Version = version }; "TX Software"
_context.TxSoftwares.Add(newSoftware); );
await _context.SaveChangesAsync();
return newSoftware;
} }
/// <summary> /// <summary>
/// 获取或创建RX类型维度 /// 获取或创建RX类型维度(并发安全)
/// </summary> /// </summary>
public async Task<RxType> GetOrCreateRxTypeAsync(string name) public async Task<RxType> GetOrCreateRxTypeAsync(string name)
{ {
var existing = await _context.RxTypes return await GetOrCreateDimensionAsync(
.FirstOrDefaultAsync(r => r.Name == name); () => _context.RxTypes.AsNoTracking().FirstOrDefaultAsync(r => r.Name == name),
() => new RxType { Name = name },
entity => _context.RxTypes.Add(entity),
name,
"RX Type"
);
}
/// <summary>
/// 通用的并发安全 GetOrCreate 模式
/// </summary>
private async Task<T> GetOrCreateDimensionAsync<T>(
Func<Task<T?>> findExisting,
Func<T> createNew,
Action<T> addToContext,
string identifier,
string dimensionName) where T : class
{
const int maxRetries = 3;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
// 使用 AsNoTracking 避免 ChangeTracker 缓存
var existing = await findExisting();
if (existing != null) return existing; if (existing != null) return existing;
var newRxType = new RxType { Name = name }; var newEntity = createNew();
_context.RxTypes.Add(newRxType); addToContext(newEntity);
try
{
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
return newRxType; _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;
}
/// <summary>
/// 检测 SQLite 唯一约束违反错误
/// </summary>
private static bool IsUniqueConstraintViolation(DbUpdateException ex)
{
return ex.InnerException is Microsoft.Data.Sqlite.SqliteException sqliteEx
&& sqliteEx.SqliteErrorCode == 19; // SQLITE_CONSTRAINT
} }
} }

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using WCTDataMiner.Core.Data; using WCTDataMiner.Core.Data;
using WCTDataMiner.Core.Models; using WCTDataMiner.Core.Models;
using WCTDataMiner.Core.Parsers; using WCTDataMiner.Core.Parsers;
@@ -6,32 +7,43 @@ using WCTDataMiner.Core.Parsers;
namespace WCTDataMiner.Core.Services; namespace WCTDataMiner.Core.Services;
/// <summary> /// <summary>
/// 测试场景服务 - 负责测试场景的创建和查询 /// 测试场景服务 - 负责测试场景的创建和查询(并发安全版本)
/// </summary> /// </summary>
public class ScenarioService public class ScenarioService
{ {
private readonly WctMinerDbContext _context; private readonly WctMinerDbContext _context;
private readonly DimensionService _dimensionService; private readonly DimensionService _dimensionService;
private readonly ILogger<ScenarioService> _logger;
public ScenarioService(WctMinerDbContext context, DimensionService dimensionService) public ScenarioService(WctMinerDbContext context, DimensionService dimensionService, ILogger<ScenarioService> logger)
{ {
_context = context; _context = context;
_dimensionService = dimensionService; _dimensionService = dimensionService;
_logger = logger;
} }
/// <summary> /// <summary>
/// 根据场景信息获取或创建测试场景 /// 根据场景信息获取或创建测试场景(并发安全)
/// </summary> /// </summary>
public async Task<TestScenario> GetOrCreateScenarioAsync(ScenarioInfo info) public async Task<TestScenario> GetOrCreateScenarioAsync(ScenarioInfo info)
{ {
_logger.LogDebug("查找/创建场景: TxPanel={TxPanel}, TxHardware={TxHardware}, TxSoftware={TxSoftware}, RxType={RxType}",
info.TxPanel, info.TxHardware, info.TxSoftware, info.RxType);
// 1. 获取或创建各维度 // 1. 获取或创建各维度
var txPanel = await _dimensionService.GetOrCreateTxPanelAsync(info.TxPanel); var txPanel = await _dimensionService.GetOrCreateTxPanelAsync(info.TxPanel);
var txHardware = await _dimensionService.GetOrCreateTxHardwareAsync(info.TxHardware); var txHardware = await _dimensionService.GetOrCreateTxHardwareAsync(info.TxHardware);
var txSoftware = await _dimensionService.GetOrCreateTxSoftwareAsync(info.TxSoftware); var txSoftware = await _dimensionService.GetOrCreateTxSoftwareAsync(info.TxSoftware);
var rxType = await _dimensionService.GetOrCreateRxTypeAsync(info.RxType); var rxType = await _dimensionService.GetOrCreateRxTypeAsync(info.RxType);
// 2. 检查场景是否已存在(利用唯一约束) // 2. 并发安全的场景创建
const int maxRetries = 3;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
// 使用 AsNoTracking 避免 ChangeTracker 缓存
var existing = await _context.TestScenarios var existing = await _context.TestScenarios
.AsNoTracking()
.FirstOrDefaultAsync(s => .FirstOrDefaultAsync(s =>
s.TxPanelId == txPanel.Id && s.TxPanelId == txPanel.Id &&
s.TxHardwareId == txHardware.Id && s.TxHardwareId == txHardware.Id &&
@@ -41,7 +53,11 @@ public class ScenarioService
s.TestDate == info.TestDate && s.TestDate == info.TestDate &&
s.TestSequence == info.TestSequence); s.TestSequence == info.TestSequence);
if (existing != null) return existing; if (existing != null)
{
_logger.LogDebug("场景已存在: {ScenarioId}", existing.Id);
return existing;
}
// 3. 创建新场景 // 3. 创建新场景
var scenario = new TestScenario var scenario = new TestScenario
@@ -56,10 +72,49 @@ public class ScenarioService
}; };
_context.TestScenarios.Add(scenario); _context.TestScenarios.Add(scenario);
try
{
await _context.SaveChangesAsync(); 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; 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 &&
s.TxSoftwareId == txSoftware.Id &&
s.RxTypeId == rxType.Id &&
s.TestPurpose == info.TestPurpose &&
s.TestDate == info.TestDate &&
s.TestSequence == info.TestSequence);
if (finalScenario == null)
{
throw new InvalidOperationException($"无法获取或创建场景: {info.TxPanel}/{info.TxHardware}/{info.RxType}");
}
return finalScenario;
}
/// <summary> /// <summary>
/// 获取场景详情(含维度属性) /// 获取场景详情(含维度属性)
@@ -73,4 +128,13 @@ public class ScenarioService
.Include(s => s.RxType) .Include(s => s.RxType)
.FirstOrDefaultAsync(s => s.Id == scenarioId); .FirstOrDefaultAsync(s => s.Id == scenarioId);
} }
/// <summary>
/// 检测 SQLite 唯一约束违反错误
/// </summary>
private static bool IsUniqueConstraintViolation(DbUpdateException ex)
{
return ex.InnerException is Microsoft.Data.Sqlite.SqliteException sqliteEx
&& sqliteEx.SqliteErrorCode == 19; // SQLITE_CONSTRAINT
}
} }