#260520 新增问题分析
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
让我再看一下日志中关于P盘数据的匹配情况,以及现场数据中P盘文件的实际内容:
|
||||
|
||||
现在让我查看FormMain.cs中调用FileSorter的部分,以及分发任务的配置:
|
||||
|
||||
现在让我查看日志的最后部分,看看最终状态:
|
||||
|
||||
现在我已经收集了足够的信息来进行综合分析。让我总结一下发现的问题:
|
||||
|
||||
---
|
||||
|
||||
## 问题分析报告
|
||||
|
||||
根据日志、代码和现场数据的综合分析,发现以下**核心问题**:
|
||||
|
||||
---
|
||||
|
||||
### 问题一:分发任务配置不完整 — 只配置了L551_21MY的分发规则
|
||||
|
||||
**现象:** 在整个63000行日志中,`ProcessFiles` 分发定时器(每15秒触发一次)**只执行了一个分发任务**:
|
||||
|
||||
```
|
||||
源路径: P:\, 目标路径: K:\L551R 21MY, 匹配字符: L551_21MY 位置:R
|
||||
```
|
||||
|
||||
**但根据你提供的配置截图,数据库中配置了6个分发任务**(L551R、L551L、E03L、E03R、E0YL、E0YR),而日志中只出现了第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个任务(L551L、E03L、E03R、E0YL、E0YR)从未执行。
|
||||
|
||||
**方案:**
|
||||
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 | 修复1:SQLHelper线程安全 | DAL层全局 | 低(改为标准using模式) |
|
||||
| P0 | 修复3:防止定时器重入 | FormMain | 低(加一个标志位) |
|
||||
| P1 | 修复2:消除全局变量竞争 | FileSorter + FormMain | 中(需要重构接口) |
|
||||
| P1 | 修复4:确认分发任务加载 | 需要现场数据库验证 | 低 |
|
||||
| P2 | 修复5:解耦解析与分发 | FileSorter核心逻辑 | 中(架构调整) |
|
||||
| P2 | 修复6:防御性检查 | DAL层 | 低 |
|
||||
|
||||
---
|
||||
|
||||
你希望我从哪个修复项开始实施?还是需要我对某个方案做进一步细化?
|
||||
Reference in New Issue
Block a user