
1. 项目概述为什么我们需要在ABAP里“搞并发”如果你在SAP圈子里待过几年尤其是处理过那些需要批量处理海量数据、或者用户抱怨“点一下按钮要等一分钟”的报表或接口那你一定对“性能”这两个字深有感触。我最早接触ABAP并发就是因为一个财务月末关账的报表。业务部门导出一个月的凭证明细系统吭哧吭哧跑了将近二十分钟期间用户不敢做任何操作生怕程序中断。那时候我就在想ABAP这个看似“笨重”的语言难道就只能这样单线程地、一条记录接一条记录地处理吗答案当然是否定的。ABAP并发执行函数特别是通过异步RFCRemote Function Call的方式就是我们打破这个性能瓶颈的关键武器。简单来说它允许我们将一个庞大的、耗时的任务拆分成多个独立的子任务然后同时或者说近乎同时扔给系统后台去执行。这就像以前你一个人吭哧吭哧搬一百箱货现在你叫来十个帮手每人搬十箱效率瞬间提升。在ABAP的世界里这个“帮手”就是异步RFC调用的后台工作进程。这个项目的核心就是深入探讨如何安全、高效、可控地运用这个“分身术”来解决实际开发中遇到的性能顽疾。无论是处理十万条级的BOM展开、大批量的IDoc入站处理还是复杂的物料账计算掌握并发技术都能让你从“焦头烂额”变得“游刃有余”。2. 核心思路拆解同步、异步、队列与回调在动手写代码之前我们必须把几个核心概念和它们之间的关系理清楚。ABAP里处理函数调用主要有三种RFC模式同步RFC、异步RFC和事务性RFC。我们搞并发主角是异步RFC。2.1 同步 vs 异步阻塞与解放想象一下打电话同步RFC和发微信异步RFC。打电话时你必须等对方接听、说完、挂断这个过程中你什么也干不了阻塞。发微信则不同你消息一发出去就可以去干别的事了对方可能稍后回复非阻塞。在ABAP中同步RFC (sRFC)CALL FUNCTION ... DESTINATION ...。调用程序会等待远程函数执行完毕并返回结果期间调用者被阻塞。这适用于需要立即得到结果的场景。异步RFC (aRFC)CALL FUNCTION ... STARTING NEW TASK ...。调用程序“点火”后立即返回不会等待函数执行结束。被调函数在一个独立的、新的ABAP工作进程中执行。调用者和执行者彻底脱钩这才是实现并发的基石。2.2 异步RFC的并发模型并非真正的“并行”这里有一个至关重要的理解ABAP的异步RFC并发本质上是利用系统空闲的工作进程进行“时间片”上的并发而非多核CPU上的真正并行。SAP应用服务器有一组工作进程如对话进程、后台进程等。当你发起一个异步RFC调用时系统会尝试分配一个可用的后台工作进程来执行它。如果同时发起多个调用它们会被放入队列由调度器分配可用的进程去执行。因此并发度受限于系统可用后台工作进程的数量参数rdisp/wp_no_btc。盲目发起成千上万个任务只会导致队列堆积不会无限提速。2.3 核心组件任务名、回调与参数传递一个健壮的异步RFC并发框架需要处理好以下几个要素任务名 (Task Name)每个异步调用都需要一个唯一标识符用于后续查询状态或接收回调。通常我们用GUID或者“主键索引”来生成。回调子程序 (Callback Subroutine)这是异步RFC的精髓。你可以在CALL FUNCTION ... STARTING NEW TASK ... CALLING ... ON END OF TASK中指定一个回调子程序。当异步任务执行结束时无论成功或失败系统会自动调用这个子程序你可以在里面处理结果、收集数据或更新状态。没有回调的异步RFC是“盲打”你无法知道它何时结束、结果如何。参数传递异步RFC的参数传递有特殊限制。IMPORTING、EXPORTING、CHANGING参数都必须是“按值传递”的也就是说只能传递数据本身不能传递引用如内表指针。对于复杂的内表通常需要EXPORTING到内存ID或者在函数内部直接读取数据库。2.4 与“锁”的博弈提到并发绝对绕不开“锁”。当多个异步任务同时去更新同一张数据库表比如同一张物料凭证的同一条记录时就会发生经典的“丢失更新”或“脏读”问题。ABAP通过ENQUEUE和DEQUEUE函数即锁对象来管理应用层锁。在设计并发任务时必须仔细规划数据分区策略尽量让不同的任务处理不同的数据子集例如按公司代码、工厂、物料类型分组从根源上减少锁冲突。如果冲突不可避免则需要设计重试机制或更精细的锁管理策略。3. 详细实现步骤与代码解析下面我将通过一个模拟“批量更新物料价格”的经典场景来展示一个完整的、具备异常处理和结果收集的异步RFC并发实现。3.1 场景定义与任务拆分假设我们需要根据最新采购信息更新10万种物料的标准价格。单线程循环10万次UPDATE或MODIFY速度慢且容易超时。我们决定按物料类型进行拆分比如有5种主要的物料类型ROH, HALB, FERT, HAWA, DIEN我们就创建5个并发任务每个任务处理属于该类型的所有物料。3.2 主调程序结构主程序负责准备数据、发起异步调用、等待所有任务完成并处理最终结果。REPORT zrfc_mass_update_price_concurrent. DATA: gt_task_list TYPE TABLE OF string, “ 存储所有任务名 gv_task_name TYPE string, gv_index TYPE i VALUE 0. DATA: gt_material_types TYPE RANGE OF mara-mtart. DATA: gt_results TYPE TABLE OF ty_result. “ 自定义结果类型 DATA: gv_finished_tasks TYPE i. “ 1. 确定要处理的物料类型范围 gt_material_types VALUE #( ( sign I option EQ low ROH ) ( sign I option EQ low HALB ) ( sign I option EQ low FERT ) ( sign I option EQ low HAWA ) ( sign I option EQ low DIEN ) ). “ 2. 为每个物料类型发起一个异步任务 LOOP AT gt_material_types ASSIGNING FIELD-SYMBOL(fs_type). gv_index gv_index 1. gv_task_name |UPDATE_PRICE_{ fs_type-low }_{ gv_index }|. APPEND gv_task_name TO gt_task_list. CALL FUNCTION Z_UPDATE_PRICE_BY_MTART “ 被异步调用的函数 STARTING NEW TASK gv_task_name DESTINATION IN GROUP DEFAULT “ 使用默认服务器组 CALLING z_callback_on_task_end ON END OF TASK EXPORTING iv_material_type fs_type-low EXCEPTIONS system_failure 1 MESSAGE lv_message communication_failure 2 MESSAGE lv_message resource_failure 3. “ 如果后台工作进程不足会触发此异常 IF sy-subrc 0. “ 处理立即发现的错误如资源不足 APPEND VALUE ty_result( task_name gv_task_name status E message lv_message ) TO gt_results. DELETE gt_task_list WHERE table_line gv_task_name. “ 从待等待列表中移除 ENDIF. ENDLOOP. “ 3. 等待所有剩余任务完成 WAIT UNTIL gv_finished_tasks lines( gt_task_list ). “ 或者使用更可控的循环等待 “ WHILE lines( gt_task_list ) 0. “ WAIT FOR ASYNCHRONOUS TASKS UNTIL lines( gt_task_list ) 0. “ ENDWHILE. “ 4. 所有任务完成后的汇总处理 PERFORM display_summary USING gt_results.3.3 被异步调用的函数模块 Z_UPDATE_PRICE_BY_MTART这个函数必须被标记为“支持远程调用”。它的逻辑是独立的。FUNCTION z_update_price_by_mtart. *---------------------------------------------------------------------- **Local Interface: * IMPORTING * VALUE(IV_MATERIAL_TYPE) TYPE MTART * EXPORTING * VALUE(EV_PROCESSED_COUNT) TYPE I * VALUE(EV_ERROR_COUNT) TYPE I * VALUE(ET_ERROR_LOG) TYPE ZTT_ERROR_LOG * EXCEPTIONS * PROCESSING_ERROR *---------------------------------------------------------------------- DATA: lt_materials TYPE TABLE OF mara, lt_mbew TYPE TABLE OF mbew, lv_processed TYPE i, lv_error TYPE i. “ 1. 读取该类型下所有需要更新的物料 SELECT matnr FROM mara INTO TABLE lt_materials WHERE mtart iv_material_type AND lvorm abap_true. “ 未删除标记的 “ 2. 根据业务逻辑计算新价格这里简化为例 LOOP AT lt_materials ASSIGNING FIELD-SYMBOL(fs_mat). “ 模拟复杂的价格计算逻辑 DATA(lv_new_price) cl_calculationget_standard_price( fs_mat-matnr ). “ 3. 更新数据库表 MBEW UPDATE mbew SET stprs lv_new_price, peinh 1 WHERE matnr fs_mat-matnr AND bwkey ‘1000’. “ 假设工厂1000 IF sy-subrc 0. lv_processed lv_processed 1. COMMIT WORK. “ 关键点每个更新后立即提交还是批量提交 ELSE. lv_error lv_error 1. APPEND VALUE zst_error_log( matnr fs_mat-matnr message ‘Update failed’ ) TO et_error_log. ENDIF. ENDLOOP. ev_processed_count lv_processed. ev_error_count lv_error. “ 如果整个任务失败如数据库连接中断可以抛出 PROCESSING_ERROR IF lv_error 100. “ 假设错误超过100个视为任务失败 RAISE processing_error. ENDIF. ENDFUNCTION.注意关于 COMMIT WORK 的位置这是在并发更新中最容易踩坑的地方之一。在上面的例子里我在循环内每条更新后都执行了COMMIT WORK。这有利于尽快释放数据库锁减少其他任务等待的时间但会显著增加数据库提交的次数带来性能开销。另一种策略是在函数末尾统一提交一次。选择哪种取决于你的业务容忍度和对锁冲突的预判。如果更新冲突风险高建议尽早提交。3.4 回调子程序 Z_CALLBACK_ON_TASK_END这是接收和处理每个任务结果的枢纽。FORM z_callback_on_task_end USING p_taskname TYPE clike. DATA: lv_processed TYPE i, lv_error TYPE i, lt_errors TYPE ztt_error_log. “ 1. 接收任务返回的参数 RECEIVE RESULTS FROM FUNCTION Z_UPDATE_PRICE_BY_MTART IMPORTING ev_processed_count lv_processed ev_error_count lv_error et_error_log lt_errors EXCEPTIONS others 1. IF sy-subrc 0. “ 接收结果失败可能是任务异常终止 lv_error lv_error 1. “ 或者进行特殊错误记录 ENDIF. “ 2. 将任务结果收集到全局内表 APPEND VALUE ty_result( task_name p_taskname processed lv_processed errors lv_error status COND #( WHEN lv_error 0 THEN S ELSE E ) ) TO gt_results. “ 3. 从待等待列表中移除已完成的任务名 DELETE gt_task_list WHERE table_line p_taskname. “ 4. 更新已完成任务计数器用于WAIT UNTIL条件 gv_finished_tasks gv_finished_tasks 1. “ 5. 可选实时更新监控屏幕或应用日志 MESSAGE s398(00) WITH ‘Task completed:’ p_taskname lv_processed ‘materials processed.’. ENDFORM.4. 性能调优与关键参数实现基本功能只是第一步要让并发方案真正高效稳定必须关注以下几个调优点。4.1 并发度的控制不要贪多并发任务数不是越多越好。它受到两个主要限制系统后台工作进程数通过事务码RZ12或SM50/SM66查看wp_no_btc参数。你的最大有效并发数最好略低于这个值为系统预留处理其他后台作业的空间。通常设置为可用后台进程数的60%-80%是个安全的选择。数据库负载与锁竞争即使ABAP层能发起很多任务它们最终都会落到数据库上执行UPDATE。过多的并发UPDATE会导致数据库锁队列激增可能引发死锁或性能雪崩。务必进行压力测试找到一个针对特定业务场景的最佳并发数。4.2 数据分区策略减少锁冲突的关键这是设计阶段最重要的决策。好的分区能让任务间几乎无冲突差的则会导致大量回滚和重试。理想分区按主键范围划分如物料号区间、凭证编号区间。确保每个任务操作的数据集是互斥的。次优分区按逻辑字段如工厂、物料类型、公司代码。冲突概率较低但同一工厂下的热门物料仍可能冲突。应避免让所有任务都能处理所有数据然后通过应用锁来排队。这相当于把并行又拉回了串行。4.3 内存与资源管理每个异步RFC任务都是一个独立的工作进程会消耗内存和数据库连接。在处理海量数据时要警惕避免在异步函数中操作巨型内表这会导致每个工作进程都占用大量内存。优先让函数自己从数据库读取所需数据。及时关闭资源如果函数中打开了文件、HTTP连接等务必在函数结束前正确关闭。使用EXPORT TO DATABASE或共享内存对于需要传递给异步任务的、只读的巨型参考数据可以提前存放到共享内存区域供所有任务读取避免重复传输。4.4 错误处理与任务状态监控异步任务的错误是“静默”的不会直接导致主程序DUMP。因此一个健壮的框架必须包含回调中的全面接收在RECEIVE RESULTS时必须处理所有EXCEPTIONS。超时机制使用WAIT FOR ASYNCHRONOUS TASKS ... UP TO n SECONDS为主程序设置总等待超时防止个别“僵尸任务”导致主程序永远挂起。任务状态查询可以通过CL_SYSTEM_TRANSACTION_STATES类来查询特定任务名的状态用于构建更复杂的监控界面。结果汇总与重试队列主程序应收集所有失败记录并可能将其放入一个重试队列进行第二次、更温和的可能是串行的处理。5. 常见陷阱与实战心得这些经验很多是在踩坑后总结的文档里往往不会写。5.1 陷阱一在异步RFC中调用弹出对话框这是绝对禁止的。异步RFC在后台进程执行没有用户会话。任何试图弹出对话框如MESSAGE ... TYPE I或CALL SCREEN的操作都会导致任务立即失败。所有消息都必须通过EXPORTING参数或异常传递回主程序由主程序统一向用户报告。5.2 陷阱二共享资源的竞态条件除了数据库锁还要注意ABAP共享对象的竞态条件。例如如果多个异步任务同时向同一个应用服务器文件写入日志或者更新同一个共享内存区域就需要使用ENQUEUE进行同步控制。一个简单的办法是为每个任务创建独立的日志对象或文件。5.3 陷阱三忽略COMMIT WORK和ROLLBACK WORK的作用域记住每个异步RFC任务都有自己的LUW逻辑工作单元。在任务A中执行的COMMIT WORK只会提交任务A自己的数据库更新不会影响任务B。同样任务B的ROLLBACK WORK也不会回滚任务A的更改。这意味着你无法跨任务实现一个“全有或全无”的分布式事务。如果业务上要求所有子任务必须全部成功否则全部回滚那么异步RFC可能不是最合适的方案需要考虑其他模式如带有补偿操作的事务性RFC。5.4 心得一从小规模开始逐步增加并发度不要一开始就设定并发度为50。先从2-3个任务开始观察系统负载STAD、数据库锁DBACOCKPIT或SM12和更新性能。逐步增加并发数直到找到性能提升的拐点即再增加并发数总耗时不再下降甚至上升。这个拐点就是当前系统和业务场景下的最优并发度。5.5 心得二为任务名赋予业务含义任务名不要只用GUID。像例子中那样使用UPDATE_PRICE_FERT_01这样的命名在排查问题比如在SM50中看到哪个任务卡住了时一眼就能知道这个进程在干什么极大提升调试效率。5.6 心得三考虑使用CL_PARALLEL框架对于SAP NetWeaver 7.0及以上版本SAP提供了更高级的并行处理类CL_PARALLEL。它内部封装了异步RFC的复杂性提供了更优雅的Map-Reduce编程模型。如果你的场景符合“将输入集合拆分、并行处理、再合并结果”的模式强烈建议先评估CL_PARALLEL它可能比手动管理一堆异步RFC更简洁、更安全。6. 一个更复杂的案例带流量控制的并发处理有时我们需要处理的不是固定的几个分组而是成千上万个独立的任务项例如处理10万个独立的IDoc。直接发起10万个异步调用是灾难。这时需要引入生产者-消费者模型和流量控制。6.1 设计思路主程序作为“生产者”从数据库读取所有待处理的任务项如IDoc编号放入一个全局的待处理队列可以用共享内存或一个自定义的数据库锁表来实现。 然后启动一个固定数量的“消费者”比如8个异步RFC函数。每个消费者函数被设计成一个循环从全局队列中“领取”一个任务项需要原子操作用ENQUEUE保护处理它然后继续领取下一个直到队列为空。6.2 关键实现片段“ 消费者函数 Z_IDOC_PROCESSOR_WORKER FUNCTION z_idoc_processor_worker. DATA: lv_idoc_number TYPE edidc-docnum, lv_queue_empty TYPE abap_bool. WHILE abap_true. “ 1. 原子性地从全局队列获取一个IDoc号 TRY. cl_global_queueget_next_idoc( IMPORTING ev_idoc_number lv_idoc_number ev_queue_empty lv_queue_empty ). CATCH cx_root. EXIT. “ 获取失败退出循环 ENDTRY. IF lv_queue_empty abap_true. EXIT. “ 队列已空任务结束 ENDIF. “ 2. 处理这个具体的IDoc PROCESS_IDOC( lv_idoc_number ). “ 3. 将处理结果标记到另一个结果表 MARK_IDOC_AS_PROCESSED( lv_idoc_number ). ENDWHILE. ENDFUNCTION.主程序只需要启动固定数量如8个的Z_IDOC_PROCESSOR_WORKER异步任务即可。这种方式完美控制了并发度避免了资源耗尽并能优雅地处理动态任务列表。7. 监控与调试技巧当并发程序跑起来后如何知道它是否健康7.1 使用事务码监控SM50/SM66 (工作进程概览)查看所有活动的工作进程。你的异步任务会显示为“RFC”类型进程文本就是你指定的任务名。在这里可以看到哪些任务正在运行、运行了多久、消耗了多少CPU/内存。SM12 (锁条目概览)查看数据库锁。如果并发任务卡住首先来这里检查是否有锁等待或死锁。STAD (负载监控)事后分析工具可以查看程序在特定时间段的性能数据了解每个异步任务对数据库、CPU的消耗。7.2 在代码中植入性能探针在异步函数的开始和结束处使用GET RUN TIME或CL_ABAP_RUNTIMEGET_RUNTIME记录执行时间并通过EXPORTING参数返回。在主程序的回调中收集这些时间可以帮你分析任务负载是否均衡如果某个任务时间远长于其他说明数据分区可能不均。7.3 结构化日志记录不要只用MESSAGE。为每个任务创建一个结构化的日志内表记录开始时间、结束时间、处理记录数、错误详情等。所有任务的日志最终汇总到主程序可以统一写入应用日志表如BAL便于后续分析和审计。异步RFC并发编程是ABAP开发者从“实现功能”到“设计高性能应用”进阶的必经之路。它要求开发者具备更全面的视角不仅关注业务逻辑更要关注系统资源、数据一致性和错误恢复。开始时可能会觉得比写单线程程序复杂数倍但一旦掌握你就能解决那些曾经令人望而生畏的性能难题真正释放SAP应用服务器的处理潜力。我的建议是找一个非关键的业务场景从最简单的例子开始动手实践逐步增加复杂度积累的经验会让你在面对核心业务性能优化时充满信心。