fix: 修复并发安全问题导致的数据库崩溃和数据错乱

问题根因:
FileSortTimer 每15秒触发 Task.Run 执行 ProcessFiles(),当P盘文件量大时
上一轮未完成新一轮又启动,多线程并发操作同一数据库连接和全局变量,导致:

1. SQLHelper 静态 SqlConnection/SqlCommand/SqlDataReader 被多线程覆盖,连接状态异常
2. ConfigDfn.strEquipNo 等全局静态变量被并发覆盖,事件触发时使用错误车号
3. 定时器无重入保护,多个 ProcessFiles() 实例并发执行
4. HasBothSidesMeasureResult 查询返回空表时直接访问列导致崩溃

修复内容:

[P0] SQLHelper.cs - 线程安全改造
- 删除静态 conn/cmd/sdr 字段和 GetConn() 方法
- 所有方法(ExecuteQuery/ExecuteNonQuery/ExecuteDs/BulkCopy)改为 using 独立连接模式
- 每次调用创建独立 SqlConnection,彻底消除连接竞争

[P0] FormMain.cs - 防止定时器重入
- 新增 _isProcessing 原子标志位(Interlocked.CompareExchange)
- FileSortTimer_Tick 检测到上一轮仍在执行时跳过本次触发
- finally 块确保标志位一定被释放

[P1] FileSorter.cs - 消除全局变量竞争
- 新增 CsvParseResult 类封装解析结果(CarID/CarModel/Position/MeasureTime)
- ImportCsv2Sql 改为返回 CsvParseResult,不再写入 ConfigDfn 全局变量
- GenerateSingleSideStatistics 改为参数传入(carID/carModel/groupName/position/measureTime)
- GenCustomerReport/GenerateCsvReport 改为参数传入 carModel
- OnFileParsed 事件改为三参数签名(carID, position, measureTime)

[P1] FormMain.cs - UI事件适配
- FileSorter_OnFileParsed 适配新的三参数签名
- DisplayMeasureData 接收 measureTime 参数,不再从全局变量读取

[P2] CjlrDAL.cs - 防御性检查
- HasBothSidesMeasureResult 增加 carId 空值检查
- 增加 dt.Columns.Contains("HasBothSides") 检查,避免空表异常

[P2] FileSorter.cs - 增强日志
- ProcessFiles() 开头打印查询到的任务总数,便于现场确认任务是否全部加载

影响文件:
- Analysis/DAL/SQLHelper.cs
- Analysis/CjlrForm/FileSorter.cs
- Analysis/FormMain.cs
- Analysis/DAL/CjlrDAL.cs
This commit is contained in:
zhengxuan.zhang
2026-05-20 11:35:33 +08:00
parent 690dfcda32
commit 172a29f082
4 changed files with 767 additions and 124 deletions
+83 -110
View File
@@ -7,36 +7,10 @@ namespace NSAnalysis.DAL
{
public class SQLHelper
{
private static SqlConnection conn = null;
private static SqlCommand cmd = null;
private static SqlDataReader sdr = null;
public static string connStr = "";
public static int iFlag = 0;
private static SqlConnection GetConn()
{
conn = new SqlConnection(connStr);
try
{
if (conn.State == ConnectionState.Closed)
{
conn.Open();
}
return conn;
}
catch (Exception ex)
{
if (iFlag == 0)
{
iFlag++;
MessageBox.Show("数据库打开连接失败,请检查数据库是否正确连接!原因:" + ex.ToString(), "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
// 这里建议抛出异常而不是返回未打开的连接
throw;
}
}
#region SQL语句或存储过程 int类型
/// <summary>
@@ -47,25 +21,15 @@ namespace NSAnalysis.DAL
/// <returns>返回受影响的行数</returns>
public static int ExecuteNonQuery(string cmdText, CommandType ct)
{
int res = 0;
try
using (var conn = new SqlConnection(connStr))
{
cmd = new SqlCommand(cmdText, GetConn());
cmd.CommandType = ct;
res = cmd.ExecuteNonQuery(); //返回受影响的行数
}
catch (Exception ex)
{
throw ex;
}
finally
{
if (conn.State == ConnectionState.Open)
conn.Open();
using (var cmd = new SqlCommand(cmdText, conn))
{
conn.Close();
cmd.CommandType = ct;
return cmd.ExecuteNonQuery();
}
}
return res;
}
#endregion SQL语句或存储过程 int类型
@@ -80,7 +44,6 @@ namespace NSAnalysis.DAL
/// <returns>返回受影响的行数</returns>
public static int ExecuteNonQuery(string cmdText, SqlParameter[] paras, CommandType ct)
{
int res = 0;
using (var conn = new SqlConnection(connStr))
{
conn.Open();
@@ -88,10 +51,9 @@ namespace NSAnalysis.DAL
{
cmd.CommandType = ct;
cmd.Parameters.AddRange(paras);
res = cmd.ExecuteNonQuery();
return cmd.ExecuteNonQuery();
}
}
return res;
}
#endregion SQL语句或存储过程 int类型
@@ -107,11 +69,17 @@ namespace NSAnalysis.DAL
public static DataTable ExecuteQuery(string cmdText, CommandType ct)
{
DataTable dt = new DataTable();
cmd = new SqlCommand(cmdText, GetConn());
cmd.CommandType = ct;
using (sdr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
using (var conn = new SqlConnection(connStr))
{
dt.Load(sdr);
conn.Open();
using (var cmd = new SqlCommand(cmdText, conn))
{
cmd.CommandType = ct;
using (var sdr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
{
dt.Load(sdr);
}
}
}
return dt;
}
@@ -130,12 +98,18 @@ namespace NSAnalysis.DAL
public static DataTable ExecuteQuery(string cmdText, SqlParameter[] paras, CommandType ct)
{
DataTable dt = new DataTable();
cmd = new SqlCommand(cmdText, GetConn());
cmd.CommandType = ct;
cmd.Parameters.AddRange(paras);
using (sdr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
using (var conn = new SqlConnection(connStr))
{
dt.Load(sdr);
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;
}
@@ -149,11 +123,15 @@ namespace NSAnalysis.DAL
/// <returns></returns>
public static DataSet ExecuteDs(String Sqlstr)
{
using (SqlDataAdapter da = new SqlDataAdapter(Sqlstr, GetConn()))
using (var conn = new SqlConnection(connStr))
{
DataSet ds = new DataSet();
da.Fill(ds);
return ds;
conn.Open();
using (SqlDataAdapter da = new SqlDataAdapter(Sqlstr, conn))
{
DataSet ds = new DataSet();
da.Fill(ds);
return ds;
}
}
}
@@ -214,28 +192,32 @@ namespace NSAnalysis.DAL
public static int InsertMeasureDataToDB(DataTable InsertDT)
{
int iResult = 1;
using (SqlBulkCopy bulkCopy = new SqlBulkCopy(GetConn()))
using (var conn = new SqlConnection(connStr))
{
try
conn.Open();
using (SqlBulkCopy bulkCopy = new SqlBulkCopy(conn))
{
bulkCopy.DestinationTableName = "TMeasureData";//要插入的表的表明,创造映射关系,比下面的直接写表名称 更加灵活
bulkCopy.ColumnMappings.Add("CarID", "CarID");//映射字段名 DataTable列名 ,数据库 对应的列名
bulkCopy.ColumnMappings.Add("CarType", "CarType");//映射字段名 DataTable列名 ,数据库 对应的列名
bulkCopy.ColumnMappings.Add("MeasPointName", "MeasPointName");
bulkCopy.ColumnMappings.Add("DimensionName", "DimensionName");
bulkCopy.ColumnMappings.Add("NormalValue", "NormalValue");
bulkCopy.ColumnMappings.Add("LowerTolVal", "LowerTolVal");
bulkCopy.ColumnMappings.Add("UpperTolVal", "UpperTolVal");
bulkCopy.ColumnMappings.Add("MeasureValue", "MeasureValue");
bulkCopy.ColumnMappings.Add("MeasureItemResult", "MeasureItemResult");
bulkCopy.ColumnMappings.Add("MeasureDate", "MeasureDate");
bulkCopy.ColumnMappings.Add("Remark", "Remark");
bulkCopy.WriteToServer(InsertDT);
}
catch (Exception ex)
{
MessageBox.Show("批量插入测量数据到数据库失败!原因:" + ex.ToString(), "提示", MessageBoxButtons.OK, MessageBoxIcon.Error);
iResult = -1;
try
{
bulkCopy.DestinationTableName = "TMeasureData";
bulkCopy.ColumnMappings.Add("CarID", "CarID");
bulkCopy.ColumnMappings.Add("CarType", "CarType");
bulkCopy.ColumnMappings.Add("MeasPointName", "MeasPointName");
bulkCopy.ColumnMappings.Add("DimensionName", "DimensionName");
bulkCopy.ColumnMappings.Add("NormalValue", "NormalValue");
bulkCopy.ColumnMappings.Add("LowerTolVal", "LowerTolVal");
bulkCopy.ColumnMappings.Add("UpperTolVal", "UpperTolVal");
bulkCopy.ColumnMappings.Add("MeasureValue", "MeasureValue");
bulkCopy.ColumnMappings.Add("MeasureItemResult", "MeasureItemResult");
bulkCopy.ColumnMappings.Add("MeasureDate", "MeasureDate");
bulkCopy.ColumnMappings.Add("Remark", "Remark");
bulkCopy.WriteToServer(InsertDT);
}
catch (Exception ex)
{
MessageBox.Show("批量插入测量数据到数据库失败!原因:" + ex.ToString(), "提示", MessageBoxButtons.OK, MessageBoxIcon.Error);
iResult = -1;
}
}
}
return iResult;
@@ -251,27 +233,26 @@ namespace NSAnalysis.DAL
/// <param name="InsertDT">要插入的数据表</param>
public static void TWorkpieceListToSQLServer(DataTable InsertDT)
{
using (SqlBulkCopy bulkCopy = new SqlBulkCopy(GetConn()))
using (var conn = new SqlConnection(connStr))
{
try
conn.Open();
using (SqlBulkCopy bulkCopy = new SqlBulkCopy(conn))
{
bulkCopy.DestinationTableName = "TWorkpieceList";//要插入的表的表明,创造映射关系,比下面的直接写表名称 更加灵活
bulkCopy.ColumnMappings.Add("WorkpieceID", "WorkpieceID");//映射字段名 DataTable列名 ,数据库 对应的列名
bulkCopy.ColumnMappings.Add("DrawerID", "DrawerID");
bulkCopy.ColumnMappings.Add("WorkpieceType", "WorkpieceType");
bulkCopy.ColumnMappings.Add("TrayType", "TrayType");
bulkCopy.ColumnMappings.Add("WorkpieceStatus", "WorkpieceStatus");
bulkCopy.ColumnMappings.Add("WorkpiecePos", "WorkpiecePos");
bulkCopy.WriteToServer(InsertDT);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
// Close the SqlDataReader. The SqlBulkCopy object is automatically closed at
// the end of the using block.
try
{
bulkCopy.DestinationTableName = "TWorkpieceList";
bulkCopy.ColumnMappings.Add("WorkpieceID", "WorkpieceID");
bulkCopy.ColumnMappings.Add("DrawerID", "DrawerID");
bulkCopy.ColumnMappings.Add("WorkpieceType", "WorkpieceType");
bulkCopy.ColumnMappings.Add("TrayType", "TrayType");
bulkCopy.ColumnMappings.Add("WorkpieceStatus", "WorkpieceStatus");
bulkCopy.ColumnMappings.Add("WorkpiecePos", "WorkpiecePos");
bulkCopy.WriteToServer(InsertDT);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
@@ -287,24 +268,16 @@ namespace NSAnalysis.DAL
/// <param name="InsertDataTable">数据集</param>
public static void SqlBulkCopyInsert(string strDBTableName, DataTable InsertDataTable)
{
try
using (var conn = new SqlConnection(connStr))
{
using (SqlBulkCopy sqlRevdBulkCopy = new SqlBulkCopy(GetConn()))//引用SqlBulkCopy
conn.Open();
using (SqlBulkCopy sqlRevdBulkCopy = new SqlBulkCopy(conn))
{
sqlRevdBulkCopy.DestinationTableName = strDBTableName;//数据库中对应的表名
sqlRevdBulkCopy.NotifyAfter = InsertDataTable.Rows.Count;//有几行数据
sqlRevdBulkCopy.WriteToServer(InsertDataTable);//数据导入数据库
sqlRevdBulkCopy.Close();//关闭连接
sqlRevdBulkCopy.DestinationTableName = strDBTableName;
sqlRevdBulkCopy.NotifyAfter = InsertDataTable.Rows.Count;
sqlRevdBulkCopy.WriteToServer(InsertDataTable);
}
}
catch (Exception ex)
{
Console.WriteLine("数据库处理出错,SqlBulkCopyInsert,原因:" + ex.Message);
throw (ex);
}
}
#endregion 使SqlBulkCopy将DataTable中的数据批量插入数据库中