#260520 新增问题分析

This commit is contained in:
zhengxuan.zhang
2026-05-20 10:31:25 +08:00
parent eed9d64b58
commit 690dfcda32
+332
View File
@@ -0,0 +1,332 @@
让我再看一下日志中关于P盘数据的匹配情况以及现场数据中P盘文件的实际内容
现在让我查看FormMain.cs中调用FileSorter的部分以及分发任务的配置
现在让我查看日志的最后部分看看最终状态
现在我已经收集了足够的信息来进行综合分析让我总结一下发现的问题
---
## 问题分析报告
根据日志代码和现场数据的综合分析发现以下**核心问题**
---
### 问题一分发任务配置不完整 只配置了L551_21MY的分发规则
**现象** 在整个63000行日志中`ProcessFiles` 分发定时器每15秒触发一次**只执行了一个分发任务**
```
源路径: P:\, 目标路径: K:\L551R 21MY, 匹配字符: L551_21MY 位置:R
```
**但根据你提供的配置截图数据库中配置了6个分发任务**L551RL551LE03LE03RE0YLE0YR而日志中只出现了第1条任务
**根因分析** 查看 `GetTaskRecords()` 方法它调用 `_dal.SelectTaskByCondition("", "", "start")` 查询状态为"start"的任务日志中只出现了L551R的分发任务说明
- 其他5个任务可能在数据库中的运行状态不是"启动"
- 或者第一个任务处理P盘大量文件时耗时过长P盘有数百个CSV文件导致后续任务在同一轮中未被执行到但代码逻辑是foreach遍历所有任务不应该跳过
**更可能的原因** 分发定时器在后台线程 `Task.Run` 中执行而解析入库逻辑 `AnalysisNxsCSV` 在遍历每个文件时都会执行当P盘有几百个文件时第一个任务L551R的处理时间极长15秒定时器再次触发时又启动了新的 `ProcessFiles()`导致**多个分发任务并发执行同一个P盘目录**产生竞争条件
---
### 问题二数据库连接崩溃 并发导致SQL连接池耗尽
**现象** 日志中出现大量数据库错误
- `ExecuteReader requires an open and available Connection. The connection's current state is connecting.`
- `Internal connection fatal error. Error state: 15, Token : 8`
- `Column 'Id' does not belong to table .`
- `Column 'HasBothSides' does not belong to table .`
- `Column 'CarID' does not belong to table .`
**根因**
1. `FileSortTimer_Tick` 每15秒在 `Task.Run` 中启动新的 `ProcessFiles()`
2. 同时 `CenterControl` 中的 `tmReadNextsenseCSV` 定时器500ms间隔也在调用 `AnalysisNextSenseSelfMeasureCSV()` 解析同一路径的CSV
3. **两套解析逻辑并发操作同一个P盘目录和同一个数据库**没有任何锁或互斥机制
4. SQL连接被多线程共享/竞争导致连接状态异常查询返回空DataTable后续代码访问不存在的列时抛出异常
---
### 问题三`ConfigDfn` 全局静态变量被并发覆盖
**现象** 日志中反复出现
```
触发事件,车号: T1558036 位置: R (重复几十次)
触发事件,车号: T1558437 位置: R (重复几十次)
```
**根因** `FileSorter.AnalysisNxsCSV` 中设置了全局变量
```csharp
ConfigDfn.strEquipNo = firstRecord.ProductNum;
ConfigDfn.strCarModel = firstRecord.Model;
ConfigDfn.strEquipPosition = ...
```
当多个文件并发解析时`ConfigDfn.strEquipNo` 被不同线程反复覆盖`OnFileParsed` 事件触发时使用的是被覆盖后的值导致
- 同一个车号被重复触发事件
- `DisplayMeasureData` 查询时找不到数据因为数据还没入库就被下一个文件覆盖了车号
- `HasBothSidesMeasureResult` 查询的是错误的车号
---
### 问题四文件未被分发 匹配逻辑与实际数据不匹配
**现象** P盘中的E03和E0Y文件全部"未匹配到"
**根因** 分发任务只配置了 `L551_21MY` 的匹配规则匹配特征符为 `L551_21MY_R`但P盘中的文件实际是
- E0Y车型MEASGROUPNAME = `E0Y_R20250929`包含日期后缀
- E03车型MEASGROUPNAME = `E03_R`
匹配逻辑 `MatchCsvValue` 检查CSV第3行第2列是否**包含** `L551_21MY_R`而实际值是 `E0Y_R20250929` `E03_R`自然不匹配
**结论** 现场数据库中E03和E0Y的分发任务虽然在配置界面中显示为"启动"但在日志中从未被执行到说明查询条件可能有问题或者这些任务在软件启动时的数据库状态不是"start"
---
### 问题五`ImportCsv2Sql` 失败后文件未被移走导致重复处理
**现象** 大量文件报 `Column 'Id' does not belong to table .` 错误后文件应该被移到Error目录但后续仍然在处理这些文件
**根因** 并发执行时两个线程同时处理同一个文件
1. 线程A调用 `IsFileProcessed` 返回false开始处理
2. 线程B也调用 `IsFileProcessed` 返回false也开始处理
3. 线程A处理完移动文件到Error
4. 线程B再次尝试处理时文件已不存在或者因为数据库连接异常导致 `InsertProcessedFile` 失败文件未被标记为已处理
---
## 总结根本原因
| 序号 | 问题 | 严重程度 |
|------|------|----------|
| 1 | **并发安全问题**`FileSortTimer`(Task.Run) `tmReadNextsenseCSV`(UI线程) 同时操作P盘和数据库无互斥 | 致命 |
| 2 | **全局变量竞争**`ConfigDfn.strEquipNo` 等静态变量被多线程覆盖 | 致命 |
| 3 | **分发任务未全部执行**日志中只看到L551R任务E03/E0Y任务从未执行 | 严重 |
| 4 | **数据库连接池崩溃**并发导致SQL连接异常查询返回空表 | 严重 |
| 5 | **文件重复处理**并发导致 `IsFileProcessed` 检查失效 | 中等 |
---
需要我针对这些问题提出修复方案吗
在制定方案之前我需要确认几个关键点
现在我已经完全掌握了问题的全貌以下是修复方案
---
## 修复方案
基于分析结果问题的根本原因是**并发安全****数据库连接管理**以下按优先级排列修复项
---
### 修复1致命SQLHelper 线程安全改造
**问题** `SQLHelper` 使用 `private static SqlConnection conn``private static SqlCommand cmd``private static SqlDataReader sdr` 三个静态字段多线程并发时互相覆盖导致连接状态异常
**方案** `ExecuteQuery` 和无参 `ExecuteNonQuery` 改为每次创建独立连接与带参数版本的 `ExecuteNonQuery` 保持一致它已经用了 `using (var conn = new SqlConnection(connStr))`
```csharp
// 改造后的 ExecuteQuery(无参数版)
public static DataTable ExecuteQuery(string cmdText, CommandType ct)
{
DataTable dt = new DataTable();
using (var conn = new SqlConnection(connStr))
{
conn.Open();
using (var cmd = new SqlCommand(cmdText, conn))
{
cmd.CommandType = ct;
using (var sdr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
{
dt.Load(sdr);
}
}
}
return dt;
}
// 改造后的 ExecuteQuery(带参数版)
public static DataTable ExecuteQuery(string cmdText, SqlParameter[] paras, CommandType ct)
{
DataTable dt = new DataTable();
using (var conn = new SqlConnection(connStr))
{
conn.Open();
using (var cmd = new SqlCommand(cmdText, conn))
{
cmd.CommandType = ct;
cmd.Parameters.AddRange(paras);
using (var sdr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
{
dt.Load(sdr);
}
}
}
return dt;
}
// 改造后的 ExecuteNonQuery(无参数版)
public static int ExecuteNonQuery(string cmdText, CommandType ct)
{
using (var conn = new SqlConnection(connStr))
{
conn.Open();
using (var cmd = new SqlCommand(cmdText, conn))
{
cmd.CommandType = ct;
return cmd.ExecuteNonQuery();
}
}
}
```
**同时删除类级别的静态字段** `conn``cmd``sdr`以及 `GetConn()` 方法所有使用 `GetConn()` 的地方 `ExecuteDs``TWorkpieceListToSQLServer``SqlBulkCopyInsert`也需要同步改造
---
### 修复2致命FileSorter 消除全局变量竞争
**问题** `FileSorter.ImportCsv2Sql` 中直接写入 `ConfigDfn.strEquipNo``ConfigDfn.strCarModel``ConfigDfn.strEquipPosition``ConfigDfn.strMeasureTime`多线程并发时互相覆盖
**方案** 将解析结果封装为局部变量/返回对象不再写入全局静态变量
```csharp
// 新增解析结果模型
public class CsvParseResult
{
public string CarID { get; set; }
public string CarModel { get; set; }
public string Position { get; set; }
public string GroupName { get; set; }
public string MeasureTime { get; set; }
public bool Success { get; set; }
}
```
`ImportCsv2Sql` 改为返回 `CsvParseResult`内部不再写 `ConfigDfn``AnalysisNxsCSV` 使用返回值传递给后续逻辑只在需要更新UI时通过事件将结果传回UI线程由UI线程统一写入 `ConfigDfn`如果UI仍需要
---
### 修复3致命防止 FileSortTimer 并发重入
**问题** 定时器每15秒触发一次 `Task.Run(() => fileSorter.ProcessFiles())`如果上一轮未完成P盘几百个文件新一轮又启动导致多个线程同时遍历P盘
**方案** 加入重入保护
```csharp
private int _isProcessing = 0; // 0=空闲, 1=处理中
private void FileSortTimer_Tick(object sender, EventArgs e)
{
if (Interlocked.CompareExchange(ref _isProcessing, 1, 0) != 0)
{
// 上一轮还在执行,跳过本次
return;
}
Task.Run(() =>
{
try
{
fileSorter.ProcessFiles();
}
catch (Exception ex)
{
MyBase.TraceWriteLine($"分发任务异常: {ex.Message}");
}
finally
{
Interlocked.Exchange(ref _isProcessing, 0);
}
});
}
```
---
### 修复4严重确认分发任务是否全部加载
**问题** 日志中只出现了 `L551R_C11` 一个分发任务其他5个任务L551LE03LE03RE0YLE0YR从未执行
**方案**
1. `ProcessFiles()` 开头增加日志打印查询到的任务总数和每个任务的详情
2. 检查 `SelectTaskByCondition("", "", "start")` 的SQL实现确认查询条件是否正确匹配所有"启动"状态的任务
3. 确认数据库中任务的 `taskStatus` 字段值是否确实为 "start"注意大小写
```csharp
public void ProcessFiles()
{
var tasks = GetTaskRecords();
if (tasks == null || tasks.Rows.Count == 0)
{
Trace("没有找到任何任务记录,处理终止。");
return;
}
Trace($"[ProcessFiles] 共查询到 {tasks.Rows.Count} 个分发任务");
foreach (DataRow task in tasks.Rows)
{
// ... 现有逻辑
}
}
```
---
### 修复5严重分离"解析入库""分发"的职责
**问题** 当前 `ProcessDirectory` 中对每个文件先调用 `AnalysisNxsCSV`解析入库+触发事件+检查双侧再执行分发匹配这导致
- 分发任务被解析入库逻辑严重拖慢
- 解析入库触发的 `OnFileParsed` 事件回调UI线程的 `DisplayMeasureData`而此时数据可能还没入库成功
**方案** 将解析入库和分发逻辑解耦为两个独立步骤
```
步骤1(解析入库):遍历P盘文件 → 解析CSV → 写入数据库 → 标记已处理
步骤2(分发):遍历P盘文件 → 匹配车型 → 移动文件到目标路径
```
或者更简单的方案**先完成所有文件的分发匹配和移动再触发解析入库**因为分发只需要读取CSV第3行第2列做匹配非常快而解析入库涉及数据库操作很慢
---
### 修复6中等`HasBothSidesMeasureResult` 异常处理
**问题** 当数据库连接异常时`ExecuteQuery` 返回空DataTable代码直接访问 `dt.Rows[0]["HasBothSides"]` 抛出 `Column 'HasBothSides' does not belong to table`
**方案** `HasBothSidesMeasureResult` 中增加防御性检查
```csharp
public bool HasBothSidesMeasureResult(string carId)
{
// ... SQL查询 ...
DataTable dt = SQLHelper.ExecuteQuery(sql, paras, CommandType.Text);
if (dt == null || dt.Rows.Count == 0 || !dt.Columns.Contains("HasBothSides"))
{
return false;
}
return Convert.ToInt32(dt.Rows[0]["HasBothSides"]) == 1;
}
```
---
### 实施优先级建议
| 优先级 | 修复项 | 预计影响范围 | 风险 |
|--------|--------|-------------|------|
| P0 | 修复1SQLHelper线程安全 | DAL层全局 | 改为标准using模式 |
| P0 | 修复3防止定时器重入 | FormMain | 加一个标志位 |
| P1 | 修复2消除全局变量竞争 | FileSorter + FormMain | 需要重构接口 |
| P1 | 修复4确认分发任务加载 | 需要现场数据库验证 | |
| P2 | 修复5解耦解析与分发 | FileSorter核心逻辑 | 架构调整 |
| P2 | 修复6防御性检查 | DAL层 | |
---
你希望我从哪个修复项开始实施还是需要我对某个方案做进一步细化