一次多线程批处理漏数据问题排查:我如何从日志反推出 SQL 并定位根因

张开发
2026/4/6 13:59:37 15 分钟阅读

分享文章

一次多线程批处理漏数据问题排查:我如何从日志反推出 SQL 并定位根因
摘要这篇文章复盘了一次真实的多线程批处理漏数据问题排查过程。问题表象是批处理任务执行后部分对象已经产生结果数据但在任务维度却查不到对应明细导致整条链路呈现出“前半段成功、后半段缺失”的异常状态。这类问题最大的难点在于它不是直接报错也不是全量失败而是只漏掉了一部分数据特别容易让人误判方向。本文重点分享了我如何从异常现象入手借助日志提取关键主键逐步反推出数据库查询路径并最终结合 SQL、表数据和代码链路定位到根因。适合有批处理、多线程、事务排障经验或需求的后端开发同学阅读。前言后端开发里最难排查的问题往往不是那种一眼就能看到报错的异常而是这种看起来流程已经跑完了但结果却少了一部分数据。前段时间我在项目里碰到了一次典型的多线程批处理问题。某个批处理任务执行完成后业务侧反馈说有一批对象的处理结果不一致一部分结果数据已经生成过程记录也能查到但在任务维度却查不到对应明细这种问题最麻烦的地方在于它不是“全部失败”而是“部分成功、部分缺失”。从排查角度看这比直接报错更难因为它会制造很多误导信息。这篇文章我想复盘一下这次排查过程重点分享以下几点遇到这类问题时我第一轮是怎么收缩怀疑范围的多线程日志很乱时应该怎么找突破口怎么从日志里的主键反推出 SQL最后又是如何把根因真正抠出来的一、问题背景项目中有一段批量处理逻辑用来对一批对象执行统一处理。由于一次处理的数据量比较大系统在实现上采用了多线程并发处理目的是缩短整体执行时间。正常情况下一次完整处理结束后会在数据库中形成以下几类数据批次主记录批次明细记录过程记录过程明细记录状态更新记录也就是说这不是单表写入而是一条相对完整的处理链路。某次执行后业务侧在核查结果时发现有一批对象出现异常。它们具备以下共同特征前面的结果数据已经存在某些过程记录已经落库但在批次维度中查不到对应对象的明细导致后续核对和追溯都出现困难从表面上看这像是“任务没有完全成功”但从数据库现象上看又不是“全部失败”。这种状态说明处理链路并没有完全中断而是只在某个阶段出现了断裂。二、问题现象为了避免一上来就陷进代码我先把现象理清楚。这批异常数据大致表现如下数据项状态批处理主流程已触发是结果数据已生成是过程记录表存在数据是过程明细表数据完整否批次维度可追溯否如果把这个问题换成更直白的话来说就是对象似乎处理过了但在批次里又像没处理完整。这类问题的危险点在于它会让人一开始误以为是查询条件有问题页面展示有延迟任务号关联错误明细只是暂时没刷出来但如果真按这个方向一直查下去很容易越查越偏。三、第一轮排查我先怀疑了什么遇到这种“部分成功、部分缺失”的问题我一般不会先看具体代码实现而是先列出可能性。因为如果不先收敛方向直接进代码只会把问题越看越大。1多线程是否存在部分任务执行失败既然这是批量并发处理那第一反应一定要放在并发执行本身是否有部分线程执行异常是否存在任务提交成功但结果未收集是否主线程过早返回导致后续任务未完整结束如果是这里出了问题就很容易出现“同一批次中有的对象成功有的对象丢失”。2事务边界是否不一致第二个怀疑点是事务。因为这种链路通常会涉及多张表如果这些写库动作不在同一个事务边界内或者中间异常没有被统一处理就可能导致前面的表写成功了后面的表没写进去最后表现成“数据只落了一半”3后置逻辑是否影响主链路很多系统里主流程执行完之后都会追加一些附加动作比如状态派生补充记录生成通知同步额外校验逻辑如果这些后置动作和主流程耦合太紧一旦后置逻辑抛异常就可能把主链路后半段打断。4是否只是查询口径问题最后一个怀疑点也是最容易被忽略的有没有可能并不是数据没写而只是“查法不对”。比如任务号关联有偏差状态字段未更新查询过滤条件过严页面口径和数据库口径不一致所以这类问题不能凭感觉必须回到数据库做验证。四、第二轮排查日志很多而且是多线程输出我是怎么找突破口的第一轮列完怀疑点之后接下来就要找证据。但多线程问题有个经典难点日志非常多而且线程交叉输出按时间顺序硬看几乎没有意义。所以我没有选择从头到尾扫日志而是换了一个思路按异常对象倒查日志。也就是说不从线程开始也不从方法开始而是从异常对象本身开始反向去找它在日志里的完整处理轨迹。我重点关注的日志信息在排查过程中我主要盯了这些字段业务主键对象编号任务号流水号线程名关键方法标识异常堆栈写库前后日志只要日志里这些主键没有丢就算输出顺序是乱的也依然可以把一条对象处理链路拼起来。这一步最重要的经验不是“看更多日志”而是“围绕主键查日志”。我后来采用的做法很简单先拿到异常对象列表逐个对象去日志中搜索把每个对象相关的关键主键记录下来再用这些主键去反推数据库查询路径这一轮下来问题虽然还没有直接落到根因上但至少已经从“全局异常”变成了“具体对象链路异常”。五、第三轮排查从日志反推出 SQL这一步是整个排查过程中最关键的一步。因为日志最多只能告诉你这个对象处理过这个方法执行过某个地方可能报了异常但日志不能直接告诉你数据究竟写到了哪张表写到哪一步停住了哪些表有哪些表没有是没写进去还是没关联出来所以最后还是要落到数据库验证。而问题在于日志一般不会打印完整 SQL更多时候只会打印一些业务主键或者上下文字段。因此这一步本质上就是从日志提取关键主键再反推出应该查哪些表、怎么查。我的处理思路第一步从日志中提取可串联的关键字段例如biz_idobject_idtask_notrace_norecord_no这些字段未必同一条日志里全都有但只要能在几条相关日志里拼起来就足够了。第二步确定涉及的核心表这里我用统一的通用表名表示batch_taskbatch_task_detailprocess_recordprocess_record_detailtask_state_log这里的核心思路不是“全表扫”而是先抓主链路上的关键表。第三步按链路逆向验证我最后采用的是这样一条验证路径异常对象↓日志中的 task_no / object_id / trace_no↓查 batch_task↓查 batch_task_detail↓查 process_record↓查 process_record_detail↓查 task_state_log↓对比正常对象与异常对象第四步必须找一条正常样本做对照这一点非常关键。如果只看异常数据很容易看半天也不知道到底差在哪里。所以我特意找了一条同批次里处理成功的对象和异常对象做一一对照。这一对照很快就能看出来正常对象应该经过哪些表每张表应该留下什么痕迹异常对象到底断在哪一步六、数据验证到底是哪一步丢了有了查询路径之后接下来就是真正落库验证。我这里特别建议排查这类问题时不要一上来写一个超大的联表 SQL而是按“表维度”逐步确认。1先查批次主表先确认这批对象关联的批次主记录是否存在任务号是否一致。SELECT id, task_no, task_status, created_time FROM batch_task WHERE task_no 123;这一步的目的是先判断是不是连批次主记录都没生成还是主记录存在但后面的数据没跟上2再查批次明细表确认异常对象是否真正挂到了批次明细中。SELECT id, task_no, object_id, detail_status, created_time FROM batch_task_detail WHERE task_no 123 AND object_id 456;如果这里查不到数据而批次主表又存在就说明问题大概率出现在“批次展开”这一段或者之后的逻辑里。3查过程记录表接着看该对象是否已经形成了过程记录。SELECT id, biz_id, object_id, record_no, record_status, created_time FROM process_record WHERE object_id 456 ORDER BY created_time DESC;如果这一步已经查到数据说明主流程前半段其实是走过的。4查过程明细表这一层通常最能暴露问题因为很多“主表有、明细没”的问题都会在这里浮出来。SELECT id, record_no, step_code, step_status, created_time FROM process_record_detail WHERE record_no 789 ORDER BY created_time ASC;如果主记录存在但过程明细缺失或者步骤中断就能进一步说明链路并未走完。5查状态更新记录最后看状态更新逻辑有没有执行。SELECT id, object_id, task_no, state_code, state_value, created_time FROM task_state_log WHERE object_id 456 ORDER BY created_time DESC;这一层主要用来判断是不是数据已经生成了但状态没更新还是状态更新也只执行了一半经过这一轮逐表验证我最终确认了一件事这不是查询口径问题而是链路中确实有一段写库逻辑被打断了。七、日志示例怎么从日志里拿到反推 SQL 的主键日志片段 1对象开始处理2026-04-05 10:12:31.128 INFO [batch-worker-3] c.demo.batch.BatchExecutor- start process, bizIdBIZ_1024, objectId456, taskNo123, traceNoTRACE_A001这条日志说明对象确实进入了处理流程能拿到 bizId能拿到 objectId能拿到 taskNo能拿到 traceNo这些字段足够反推出第一批 SQL。日志片段 2后置逻辑执行前后2026-04-05 10:12:31.452 INFO [batch-worker-3] c.demo.batch.PostActionService- execute post action, objectId456, taskNo123, recordNo7892026-04-05 10:12:31.467 ERROR [batch-worker-3] c.demo.batch.PostActionService- post action failed, objectId456, taskNo123, recordNo789, messageunexpected null state这组日志的作用非常大因为它能说明主流程前半段已经跑到了后置逻辑后置逻辑开始执行时已经带出了 recordNo异常点出现在链路中后段这就能把排查方向从“是不是没处理”收缩成“处理中后段被异常打断”。八、代码示例伪代码public void executeBatch(ListString objectIds, String taskNo) { ListFutureVoid futures new ArrayList(); for (String objectId : objectIds) { futures.add(executorService.submit(() - { handleSingleObject(objectId, taskNo); return null; })); } for (FutureVoid future : futures) { future.get(); } } private void handleSingleObject(String objectId, String taskNo) { // 1. 生成主处理记录 saveProcessRecord(objectId, taskNo); // 2. 生成过程明细 saveProcessDetail(objectId, taskNo); // 3. 执行后置逻辑 doPostAction(objectId, taskNo); // 4. 更新状态 updateTaskState(objectId, taskNo); }这段代码的意义不在于展示真实实现而在于向读者说明这是一个并发批处理入口对单个对象的处理是链式的后置逻辑位于主链路中后段一旦这里异常就可能导致链路不完整九、最终定位根因出在链路中后段的附加动作把日志、SQL 验证结果和代码链路串起来之后问题终于开始收口了。最终定位结果可以总结成一句话主流程前半段已执行成功但链路中后段的附加动作发生异常导致后续部分数据没有完整落库。也就是说真实情况并不是整个批处理都失败了多线程任务没有执行数据库根本没写而是前面的结果数据已经写入中后段附加动作执行时报错后续状态或明细写入被打断最终形成“前面有结果、批次里却查不到对应明细”的表象这也是为什么这个问题初看特别像“查询没查到”但实际上并不是查询问题。十、这类问题为什么特别难查回头复盘这类问题难查主要有三个原因。1它不是彻底失败而是“半成功”全量失败通常很容易判断因为现象统一。但这种“前面成功、后面断掉”的问题会制造大量误导信息。2多线程让日志阅读成本陡增单线程日志按时间顺序还能串起来多线程日志一旦交叉输出单纯通读基本没有意义。3数据链路长表又多当一个处理流程涉及主表、明细表、过程记录、状态更新和附加动作时如果没有主键串联意识很容易越查越散。十一、修复思路定位清楚根因之后修复通常分三层。1补数据针对已经漏掉的对象按如下顺序处理确认异常对象列表核查主表、明细表、过程表、状态表现状生成补数据 SQL先在测试环境验证确认无误后再处理正式环境2改代码代码层面至少要做两件事。第一重新审视后置逻辑与主流程的耦合关系。如果附加动作本质上属于“补充处理”那它不应该轻易影响主链路的完整性。第二补关键日志。建议至少补上对象开始处理日志核心表写入前后日志后置逻辑前后日志异常对象统一汇总日志3做防御性优化后续优化建议包括多线程结果统一收集不只负责提交任务主流程和附加动作尽量解耦关键链路增加校验或对账对异常对象进行集中汇总和告警十二、这次排查给我的几个经验1多线程问题先看数据再看代码代码只能告诉你“理论应该怎么跑”数据才能告诉你“实际上发生了什么”。2日志排查的关键不是数量而是主键面对大量日志最有效的方法不是全文通读而是围绕主键串链路。3异常样本一定要和正常样本对照单看异常对象很多时候只能看到“有问题”但和正常对象一对比问题往往会非常明显。4后置动作不要轻易污染主流程所有附加动作只要可能抛异常就应该尽量避免直接影响主链路结果。总结这次问题排查最难的地方不在于 SQL 本身有多复杂也不在于代码量有多大而在于它制造了一个非常迷惑的表象结果好像已经出来了但链路其实没有完整走完。最终真正帮我把问题定位出来的不是继续猜而是按下面这条路径一步步缩小范围现象确认 → 怀疑点收敛 → 日志定点排查 → 提取主键 → 反推 SQL → 对比正常与异常数据 → 结合代码链路定位根因对我来说这次排查最大的收获不是单纯修了一个问题而是再次验证了一件事复杂批处理问题最终一定要回到数据本身。

更多文章