Apex Cursors
Apex Cursors
为了解决在内存中加载海量数据极易爆堆的问题,Apex 引入了 Apex Cursors(类似 SQL 游标机制)与 Pagination Cursors。
为什么 Salesforce 要做 Apex Cursor?
过去在 Salesforce 中处理大批量数据时,开发者经常面临一个两难的尴尬局面:
-
Batch Apex 太重且容易堵塞:
-
Batch Apex 并发上限只有 5 个。在高并发业务或大型企业 Org 里,一旦有几个耗时的大批量任务在跑,Flex Queue 就会排满,导致后续业务批处理直接报错或无限延迟。
-
Batch 的生命周期完全由系统调度控制,无法自由动态决定“这批处理完我下次跳着读哪一段”。
-
-
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 采用 “两阶段:建指针 + 按需切片” 的拉取模式:
-
创建游标(不立即加载全量数据): 使用
Database.getCursor()执行 SOQL,底层只建立游标上下文,不会把 5000 万条数据全部读进 Heap。 -
按需抓取切片(
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
浙公网安备 33010602011771号