Apex Cursors

Apex Cursors

为了解决在内存中加载海量数据极易爆堆的问题,Apex 引入了 Apex Cursors(类似 SQL 游标机制)与 Pagination Cursors。

为什么 Salesforce 要做 Apex Cursor?

过去在 Salesforce 中处理大批量数据时,开发者经常面临一个两难的尴尬局面:

  1. Batch Apex 太重且容易堵塞:

    • Batch Apex 并发上限只有 5 个。在高并发业务或大型企业 Org 里,一旦有几个耗时的大批量任务在跑,Flex Queue 就会排满,导致后续业务批处理直接报错或无限延迟。

    • Batch 的生命周期完全由系统调度控制,无法自由动态决定“这批处理完我下次跳着读哪一段”。

  2. Queueable Apex 吞吐量受制于 Heap:

    • Queueable 拥有更充裕的并发与链式调用灵活性(System.enqueueJob),但它不能直接开 QueryLocator 扫 5000 万条数据。

    • 如果在 Queueable 里直接查 SOQL,单次最多只能查 50,000 条,而且数据全部加载进内存,极容易触发 Heap Size 超限(Apex CPU / Heap limit)。

Apex Cursor 正好填补了这个鸿沟: 它把 5000 万级别的数据游标能力 从 Batch 引擎中“剥离”了出来。现在你可以先执行一次游标查询生成 Cursor,把游标的 Token 传给 Queueable:

  • 每次只 fetch(0, 200) 或 fetch(offset, 2000),内存占用极小。

  • 处理完当前批次后,把最新的 offset 存下来,直接通过 System.enqueueJob 触发下一个 Queueable 继续往下读。

  • 彻底摆脱了过去只能死守那 5 个并发 Batch 槽位的限制。

  3. 一个直观的对比理解

  • Batch Apex 就像一条固定路线的传送带:你把 5000 万条数据倒在传送带起点,系统以固定每次最多 2000 个盒子打包推给你,你只能等它一个包一个包送过来,直到推完进 finish。

  • Apex Cursor 就像一把带刻度的无限标尺:数据依然在底层数据库里,内存里只有一个极轻的游标指针。你可以告诉它“我现在要第 4001 到第 6000 条”,拿到这 2000 条处理完后丢掉;下次任务被唤醒时,再直接指着标尺去抓“第 6001 到第 8000 条”。

Apex Cursor的使用场景

Apex Cursor 本身是中立的(既不是纯同步也不是纯异步),它是一个无状态的数据指针对象。

你可以在同步事务(如 Trigger、Controller、REST Service)中创建它,也可以在异步事务(如 Queueable Apex、Batch)中消费它。它最大的价值在于天然支持跨事务序列化传递,通常在同步上下文生成,在异步的 Queueable 中分批消费。

1. 它是怎么取数据的?

传统 SOQL 是一次性把记录全塞进 Apex 内存(List),而 Cursor 采用 “两阶段:建指针 + 按需切片” 的拉取模式:

  1. 创建游标(不立即加载全量数据): 使用 Database.getCursor() 执行 SOQL,底层只建立游标上下文,不会把 5000 万条数据全部读进 Heap。

  2. 按需抓取切片(fetch): 使用 cursor.fetch(position, count) 从指定索引位置按需拉取指定数量的记录进内存。

核心代码示例

Apex

 1 // 1. 生成游标(最大支持 5000 万行匹配结果,但此时并不占用 Heap)
 2 Database.Cursor locator = Database.getCursor(
 3     'SELECT Id, Name, Status__c FROM Account WHERE AnnualRevenue > 1000000 ORDER BY CreatedDate'
 4 );
 5 
 6 // 2. 获取结果总行数(元数据属性,不消耗 Heap)
 7 Integer totalRecords = locator.getNumRecords();
 8 
 9 // 3. 按偏移量(0-based)与数量取数据
10 // 单次 fetch 最大为 2,000 条,返回具体的 List 记录进当前事务 Heap
11 List<Account> chunk1 = locator.fetch(0, 200);
12 
13 // 4. 支持任意位置“跳读”或乱序读取
14 List<Account> chunk2 = locator.fetch(1000, 500);

2. 核心特性:怎么做到“跨事务”流式取数?

Cursor 对象是 可序列化(Serializable) 的。这使得你可以把它像普通的类成员变量一样保存在 Queueable 类中,在不同的 Job 链条中接力运行。

经典落地模式:Queueable 链式消费

 1 Apex
 2 public class AccountProcessorJob implements Queueable {
 3     private Database.Cursor cursor;
 4     private Integer position;
 5     private static final Integer CHUNK_SIZE = 200;
 6 
 7     // 构造函数:接收已有的游标与当前消费偏移量
 8     public AccountProcessorJob(Database.Cursor cursor, Integer position) {
 9         this.cursor = cursor;
10         this.position = position;
11     }
12 
13     public void execute(QueueableContext context) {
14         Integer total = cursor.getNumRecords();
15         
16         if (position < total) {
17             // 只抓取当前这 200 条进内存
18             List<Account> scope = cursor.fetch(position, CHUNK_SIZE);
19             
20             // 执行具体的业务逻辑与 DML
21             for (Account acc : scope) {
22                 acc.Status__c = 'Processed';
23             }
24             update scope;
25 
26             // 推进偏移量
27             Integer nextPosition = position + scope.size();
28             
29             // 链式入队,游标指针自动跨 Job 传递
30             if (nextPosition < total) {
31                 System.enqueueJob(new AccountProcessorJob(cursor, nextPosition));
32             }
33         }
34     }
35 }

3. 取数时的关键机制与注意事项

  • 游标的有效期(TTL): 游标在数据库底层有生命周期保护机制(通常最长保留 48 小时 或在会话结束时释放),过期后再次 fetch() 会抛出运行时异常,避免死指针长期占用底层临时空间。

  • 数据一致性(Snapshot vs Real-time): 游标创建时是对数据集的一个快照视图。但如果在取数期间,被标记的记录被外部事务物理硬删除(Hard Delete),fetch 会自动调整或跳过被删行,建议在查询中加入确定性排序(ORDER BY Id 或 CreatedDate)以确保分页连续性。

  • 调用频次限制: 单次 Apex 事务中最多调用 100 次 fetch()。因此在单次事务里不建议写小循环高频调用(如每次拉 10 条连续拉 100 次),而应根据业务和 Heap 承受力尽量以 200~2,000 条为一块进行批量抓取。

 

Source: Apex Cursors | Apex Developer Guide | Salesforce Developers

posted @ 2026-09-22 14:51  Victor-Wei  阅读(9)  评论(0)    收藏  举报