PAYATHON 2026

ColdFusion 11 查询循环会创建多个线程并导致 API 重复提交吗?

数据小陈

结论

普通的 for 循环不会自动为每条记录创建线程,thread action="sleep" duration=6000; 只会暂停当前线程。创建新线程的是 action="run"。这些行为可参见 Adobe 的 cfthread 文档。

现有日志表明,相同订单的处理记录出现在两个不同的执行线程上,但不能证明它们属于同一次 Scheduled Task 执行,也不能据此认定是 ColdFusion 11 的缺陷。应先检查任务入口是否被重复调用、两次执行是否重叠,以及内部方法是否存在重复调用或共享状态。

防止重复数据需要限制批处理并发,并让接收端对同一订单的重复请求实现幂等。增加 sleep 无法提供这两种保证。

日志里有哪些关键线索?

订单 VE2127772 的提交前日志出现了三次:

  • 09:12:25.456,线程 ajp-bio-8014-exec-1。
  • 09:12:37.324,仍是 ajp-bio-8014-exec-1。
  • 09:12:38.153,线程 ajp-bio-8014-exec-2。

随后,两个线程分别处理了 VE2127773。这些记录与两段订单处理流程发生重叠的情况相符,但还需要进一步确认。

同一个线程在 09:12:25 和 09:12:37 都记录了 Posting Order,中间却没有对应的 Order Posted。如果代码完全按所示路径顺序执行,正常情况下应先记录提交结果,再进入下一次循环。需要检查日志是否完整、是否有其他位置写入相同消息,以及异常处理是否重新进入了这段逻辑。

ThreadID 是执行线程名称,不是任务运行编号。线程可能被后续请求复用,因此同一线程名不能证明是同一次任务,不同线程名也不能证明循环创建了子线程。

这里的 Posting Order 只证明代码执行到了 saveApiOrder() 之前。实际发出了多少次 HTTP 请求,还要结合方法内部日志或 API 接收端日志确认。

先给每次请求和每次提交建立对应关系

在任务入口生成请求标识,在批处理方法入口生成批次标识,并将它们传到实际执行 CFHTTP 的位置。日志至少记录:

  • 请求标识、批次标识、执行节点和任务名称。
  • 当前循环序号、LOCAL.Order.id 和订单业务编号。
  • 每次 HTTP 尝试的标识、开始时间、结束时间和耗时。
  • HTTP 状态、业务响应或异常信息。

这些记录可以帮助区分:是不同请求重复启动了任务,同一请求调用了两次批处理,还是一次方法调用内部发起了多次 HTTP 请求。不要只记录订单业务编号,不同查询行也可能映射到同一个编号。

同时检查以下位置:

  1. Scheduled Task 的实际运行间隔,以及是否存在其他任务指向同一个 URL。
  2. 是否有多台服务器或多个 ColdFusion 实例都配置了相同任务。
  3. 是否存在手动运行、外部调度或其他调用入口。
  4. 调度器异常重试配置,以及 saveApiOrder() 内部的重试、递归和异常处理。
  5. getReportData() 是否返回重复订单,成功处理后是否及时更新状态。

Adobe 的 Scheduler 文档介绍了异常时重新触发任务的配置。应核对当前配置,不能假定系统已经开启或关闭了重试。

用互斥锁限制单个实例内的重叠执行

如果同一 ColdFusion 实例一次只应运行一个订单上传批次,可以在原有组件的方法中,用同名排他锁包住从读取待处理订单到完成处理的整个过程。

下面是结构示例,getReportData()、createUploadOrder() 和响应校验仍需使用现有实现:

function processOrders() {
    local.batchId = createUUID();
    local.acquired = false;

    lock
        name="MYAPP.OrderUpload.Batch"
        type="exclusive"
        timeout=1
        throwOnTimeout=false {

        local.acquired = true;
        local.qOrders = getReportData();
        local.rowNumber = 0;

        for (local.order in local.qOrders) {
            local.rowNumber++;
            local.uploadOrder = createUploadOrder();
            local.uploadOrder.setKey(local.order.id);

            writeLog(
                file="XML-Orders-Post",
                type="Information",
                text="POST_BEGIN; batchId=" & local.batchId
                    & "; row=" & local.rowNumber
                    & "; orderId=" & local.order.id
                    & "; orderReference="
                    & local.uploadOrder.getorder_reference_1()
            );

            local.orderMessages = local.uploadOrder.saveApiOrder();

            // Validate the HTTP and business response here.
            // Persist confirmed success before processing the next order.
        }
    }

    if (!local.acquired) {
        writeLog(
            file="XML-Orders-Post",
            type="Information",
            text="BATCH_SKIPPED; batchId=" & local.batchId
                & "; reason=lock timeout"
        );
    }
}

这里的 timeout=1 表示最多等待 1 秒获取锁,不会将任务运行时间限制为 1 秒。获取失败时,throwOnTimeout=false 会使代码跳过锁体,所以需要显式记录跳过事件。所有相关入口必须使用同一个锁名,查询也应放在锁内,避免在等待期间持有过时的待处理列表。

同名锁只协调单个 ColdFusion 实例内的执行,无法协调多个实例或集群节点。锁的范围和超时语义见 Adobe 的 cflock 文档。多实例部署需要数据库原子领取、共享队列或其他跨实例协调机制。

锁也不能阻止两个先后执行的批次重新提交同一订单,因此仍需正确维护订单状态。

检查对象是否真的每次都重新创建

两段代码使用的对象创建方式不同:最初使用 new UploadOrder(),后来改成了 createUploadOrder()。

需要确认该工厂方法每次都返回独立实例。如果它从 Application 等共享位置取出同一个可变对象,多个请求可能同时使用这个对象:一个请求调用 setKey() 时,另一个请求可能正在读取或提交它的状态。

即使接收变量写成 LOCAL.UploadOrder,也只说明变量属于当前函数调用,不能保证它引用的对象没有被共享。还应检查 getReportData() 和 saveApiOrder() 内部临时变量的作用域。

原代码通过 getresponse_received_datetime() 是否为日期来判断成功,这也需要核对。除非方法约定保证它只在业务成功时赋值,否则“收到响应”不能直接等同于“订单创建成功”。共享对象或未清理的旧状态也可能造成误判。

让 API 重试具有幂等性

HTTP 超时只说明调用方没有在规定时间内得到结果。接收端可能已经保存订单,但响应未能及时返回。如果此时重新提交,接收端又按新建处理,就会产生重复数据。

如果 API 支持幂等键,应按其接口约定,为同一个逻辑订单提交操作使用稳定的键。重试时继续使用原键,不能每次生成新的 UUID。

如果接收端由自己控制,可以对业务唯一键建立唯一约束,并让重复请求返回已有处理结果。发送端应保存提交状态,将确认成功、明确失败和结果未知分开处理。超时后优先按业务键查询远端结果,再决定是否重试。

如果 API 不支持幂等,也无法按业务键查询,仅靠发送端锁无法彻底消除“远端已成功、本地尚未记录成功”这个窗口。

240 秒超时为什么不能防重?

按给定配置计算,20 × (5 + 6) = 220 秒,只留下约 20 秒给查询、对象初始化、响应处理、日志和状态更新。这还建立在每次调用都符合预期耗时的前提上。

request timeout=240 是请求执行时间配置,不能用于互斥,也不能代替任务运行间隔。应根据真实耗时核对调度周期和各层超时。额外的 sleep(6000) 会延长批次运行时间,可能增加与下一次调度重叠的机会;只有 API 确实要求限速时才应保留相应等待。

优先补齐运行标识和实际 HTTP 调用日志,再限制任务并发、修正订单状态管理,并落实接收端幂等,才能同时解决触发来源不清和重复数据落库的问题。

备注:内容仅供参考。