如何避免未集成(Integration=False)的支付数据重复发送至SAP及验证逻辑位置确认
解决支付数据重复发送至SAP的问题
这是典型的异步并发场景下的状态竞态问题,核心原因在于定时器的高频触发+异步操作的时序差,导致同一条Integration=False的数据被多次读取并发送。下面我会拆解问题根源,给出具体的验证逻辑实现,以及明确每个环节的部署位置:
问题根源分析
- 定时器与异步操作的冲突:定时器每5秒触发一次,但发送SAP的异步操作可能还未完成(未更新数据库
Integration状态),下一次定时器回调就已启动,此时DataGridView中的数据仍显示Integration=False,导致重复处理。 - 依赖UI数据不可靠:仅通过DataGridView中的
Integration值判断是否发送,而UI数据和数据库实时状态存在延迟(比如ListarEmple()刷新UI不及时)。 - 无原子性状态校验:没有在数据库层面做「校验+更新」的原子操作,可能多个线程同时读取到
Integration=0,进而重复发送。
解决方案:分层验证+原子锁机制
1. 核心:数据库层面添加原子性状态锁定(必须部署)
这是避免重复发送的关键,我们需要一个存储过程,只有当数据当前Integration=0时,才将其改为1,且这个操作是原子的,确保同一时间只有一个线程能锁定这条数据。
创建存储过程:
CREATE PROCEDURE [dbo].[SP_TryLockPayment] @DocEntry INT, @IsLocked BIT OUTPUT AS BEGIN SET NOCOUNT ON; -- 原子更新:仅当Integration为0时,才修改为1 UPDATE PaymentsReceived SET Integration = 1 WHERE DocEntry = @DocEntry AND Integration = 0; -- 返回是否锁定成功:1=成功锁定,0=已被其他线程处理 SET @IsLocked = CASE WHEN @@ROWCOUNT > 0 THEN 1 ELSE 0 END; END
2. 业务逻辑层:发送前先执行锁定校验(部署在EnviarSAP方法)
在调用SAP接口之前,先调用上述存储过程尝试锁定数据,只有锁定成功的请求才继续发送;如果锁定失败,直接跳过(说明已被其他线程处理)。
修改后的EnviarSAP方法:
Consultas c = new Consultas(); public string XmlSerializado = null; public async Task<string> EnviarSAP(PagosRecibidos detalle) { bool isLocked = false; try { // 第一步:尝试锁定数据 using (var conn = new SqlConnection("你的数据库连接字符串")) { await conn.OpenAsync(); using (var cmd = new SqlCommand("SP_TryLockPayment", conn)) { cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add("@DocEntry", SqlDbType.Int).Value = detalle.DocEntry; var lockedParam = cmd.Parameters.Add("@IsLocked", SqlDbType.Bit); lockedParam.Direction = ParameterDirection.Output; await cmd.ExecuteNonQueryAsync(); isLocked = Convert.ToBoolean(lockedParam.Value); if (!isLocked) { return "数据已被其他进程处理,跳过发送"; } } } // 第二步:锁定成功,调用SAP接口 using (WSSincronizacionClient clienteSAP = new WSSincronizacionClient()) { XmlSerializado = this.SerializarXml(detalle); var respuesta = await clienteSAP.EnviarDatosSAPAsync(XmlSerializado); // 发送失败时回滚状态,允许后续重试 if (!respuesta.Contains("true|Operación Exitosa|")) { using (var conn = new SqlConnection("你的数据库连接字符串")) { await conn.OpenAsync(); using (var cmd = new SqlCommand("UPDATE PaymentsReceived SET Integration = 0 WHERE DocEntry = @DocEntry", conn)) { cmd.Parameters.Add("@DocEntry", SqlDbType.Int).Value = detalle.DocEntry; await cmd.ExecuteNonQueryAsync(); } } } c.InsertarLog(1, DateTime.Now, respuesta, XmlSerializado); return respuesta; } } catch (Exception ex) { // 异常时回滚状态 if (isLocked) { try { using (var conn = new SqlConnection("你的数据库连接字符串")) { await conn.OpenAsync(); using (var cmd = new SqlCommand("UPDATE PaymentsReceived SET Integration = 0 WHERE DocEntry = @DocEntry", conn)) { cmd.Parameters.Add("@DocEntry", SqlDbType.Int).Value = detalle.DocEntry; await cmd.ExecuteNonQueryAsync(); } } } catch { } // 忽略回滚异常,避免覆盖原错误信息 } throw ex; } }
3. 定时器回调层:优化数据获取与触发逻辑(部署在Envio_Timer_Tick)
- 定时器触发时先禁用自身,避免上一次任务未完成时再次触发;
- 直接从数据库拉取最新的
Integration=0数据,不再依赖DataGridView的缓存; - 批量处理异步任务,提升效率。
修改后的定时器回调:
private async void Envio_Timer_Tick(object sender, EventArgs e) { // 先禁用定时器,防止并发触发 Envio_Timer.Enabled = false; try { ProxyBL proxy = new ProxyBL(); // 直接从数据库获取待发送数据,避免UI缓存的延迟 var pendingPayments = GetPendingPaymentsFromDB(); // 批量处理异步任务 var sendTasks = pendingPayments.Select(pagos => proxy.EnviarPago(pagos)).ToList(); await Task.WhenAll(sendTasks); // 刷新UI和日志 ListarEmple(); ListarLog(); } catch (Exception ex) { MessageBox.Show($"发送任务出错:{ex.Message}"); } finally { // 任务完成后重新启用定时器 Envio_Timer.Enabled = true; } } // 新增从数据库获取待发送数据的方法 private List<PagosRecibidos> GetPendingPaymentsFromDB() { var paymentsList = new List<PagosRecibidos>(); using (var conn = new SqlConnection("你的数据库连接字符串")) { conn.Open(); var query = @"SELECT 'CE-'+CardCode AS CardCode,DocType,Series,DocDate, dbo.udf_GetNumeric(DocNum) AS DocNum, DocEntry,TrsfrAcct,TrsfrDate, TrsfrSum, Integration,Comments,SumApplied FROM PaymentsReceived WHERE Integration = 0"; using (var cmd = new SqlCommand(query, conn)) { using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { paymentsList.Add(new PagosRecibidos { CardCode = reader["CardCode"].ToString(), DocType = reader["DocType"].ToString(), Series = Convert.ToInt32(reader["Series"]), DocDate = Convert.ToDateTime(reader["DocDate"]), DocEntry = Convert.ToInt32(reader["DocEntry"]), TrsfrAcct = reader["TrsfrAcct"].ToString(), TrsfrDate = Convert.ToDateTime(reader["TrsfrDate"]), TrsfrSum = Convert.ToDecimal(reader["TrsfrSum"]), Integration = Convert.ToBoolean(reader["Integration"]), Comments = reader["Comments"].ToString(), SumApplied = Convert.ToDecimal(reader["SumApplied"]) }); } } } } return paymentsList; }
验证逻辑的部署环节总结
- 数据库层:原子性锁定存储过程是核心,确保数据状态的唯一性和一致性;
- 业务逻辑层(EnviarSAP):发送前执行锁定校验,过滤已被处理的数据;
- 定时器回调层:优化数据来源和触发机制,避免并发冲突和UI缓存依赖。
内容的提问来源于stack exchange,提问作者Esteban Giraldo
相关产品推荐
相关产品推荐

