Salesforce 异步 Apex 开发人员深度解析:Future、Queueable 与 Batch
背景与应用场景
作为一名 Salesforce 开发人员,在我的日常工作中,几乎无法避开与 Governor Limits (governor 限制) 的斗智斗勇。Salesforce 是一个多租户 (multi-tenant) 环境,为了保证所有客户共享的资源能够被公平、稳定地使用,平台对每一次同步事务 (synchronous transaction) 都施加了严格的资源限制,例如 SOQL 查询数量(100条)、DML 操作行数(10,000行)、以及 CPU 执行时间(10秒)等。
当我们的业务逻辑变得复杂,尤其是在处理大量数据或需要与外部系统集成时,同步执行的 Apex 代码很容易就会触碰到这些天花板。想象一下,当一个用户更新某个客户记录时,需要触发一个逻辑去更新该客户下的数千个关联的订单记录,或者需要调用一个外部的 ERP 系统来验证信用额度。这些操作很可能因为超时或超出查询限制而失败,导致用户体验极差。
这就是 Asynchronous Apex (异步 Apex) 发挥关键作用的地方。异步 Apex 允许我们将那些耗时、资源密集型的任务放到后台的一个独立线程中执行。这意味着,用户的操作可以立即完成并得到响应,而繁重的处理任务则在后台排队等待系统资源空闲时执行。这种处理模式不仅极大地提升了用户体验,更是构建可扩展、健壮的 Salesforce 应用程序的基石。
典型的应用场景包括:
- 数据批量处理:对海量数据进行清洗、迁移、或复杂的计算更新。例如,每晚对所有“待处理”的商机进行状态更新。
- 外部系统集成 (Callouts):从 Apex Trigger 或其他同步逻辑中调用外部 Web 服务。同步事务中在执行 DML 操作后是不允许直接进行 callout 的,而异步 Apex 则完美地解决了这个问题。
- 避免混合 DML 错误:在同一个事务中操作标准对象和带有特殊权限控制的 Setup Object (例如 User 对象) 会导致混合 DML 错误。将对 Setup Object 的操作放入异步方法中可以有效规避此问题。
- 定时任务:执行需要按计划运行的维护任务,如生成周报、数据归档或定期同步数据。
原理说明
Salesforce 平台提供了多种实现异步处理的机制,每种机制都有其特定的适用场景和优缺点。理解它们的核心原理是高效使用它们的前提。异步任务会被添加到一个队列中,Salesforce 会根据系统资源的可用性来调度和执行这些任务。
Future Methods (@future)
Future 方法是异步 Apex 最简单直接的一种形式。通过在方法上添加 @future
注解,我们就可以告诉平台这个方法应该在后台异步执行。它非常适合那些“即发即忘”(fire-and-forget) 的场景。
核心特点:
- 方法签名:必须是静态 (static) 方法,且返回类型必须是 void。
- 参数类型:参数必须是原始数据类型 (primitive data types, 如 Integer, String)、原始数据类型的集合 (如
List
) 或这些类型的数组。它不能接受 sObject 作为参数,这是为了防止在方法执行时,传入的 sObject 记录可能已经被修改或删除,导致数据不一致。 - 独立事务:每个 Future 方法都在自己的事务中运行,拥有独立的 Governor 限制。
- Callouts:特别适用于需要从同步代码(如 Trigger)中发起外部调用的场景,只需在注解中添加
(callout=true)
即可。
Queueable Apex (Queueable 接口)
Queueable Apex 可以看作是 Future 方法的增强版。它通过实现系统内置的 Queueable
接口,提供了更强大、更灵活的异步处理能力。
核心特点:
- 复杂参数:与 Future 方法不同,Queueable Apex 的类成员变量可以是非原始数据类型,例如 sObject 或自定义的 Apex 类型。这意味着你可以将完整的对象实例传递给异步任务。
- 任务监控:当你通过
System.enqueueJob(queueableInstance)
将一个 Queueable 任务入队时,该方法会返回一个 Job ID。你可以使用这个 ID 通过 SOQL 查询AsyncApexJob
对象来监控任务的状态(如 Queued, Processing, Completed, Failed)。 - 任务链 (Job Chaining):一个 Queueable 任务可以在其
execute
方法内部启动另一个 Queueable 任务。这是它相对于 Future 方法最显著的优势,允许你将一个复杂的业务流程分解成多个连续的异步步骤。
Batch Apex (Database.Batchable 接口)
当需要处理的数据量达到数万、数十万甚至数百万条时,Batch Apex 是不二之选。它专门为处理海量数据集而设计,将整个处理过程分解成一系列可管理的小批次 (chunks)。
核心特点:
它需要实现 Database.Batchable
接口,该接口包含三个必须实现的方法:
start(context)
:在批处理任务开始时调用,仅执行一次。它的作用是收集需要处理的所有记录,并返回一个Database.QueryLocator
或一个Iterable
。QueryLocator 最多可以处理 5000 万条记录,是最高效的方式。execute(context, scope)
:这是批处理的核心。平台会将start
方法返回的记录集分割成多个小批次(默认大小为 200),然后为每个批次调用一次execute
方法。所有的核心业务逻辑都在这里实现。每个execute
的执行都有自己独立的 Governor 限制。finish(context)
:当所有批次都处理完毕后,该方法被调用一次。通常用于执行一些总结性的操作,比如发送一封通知邮件,或者启动后续的处理任务。
此外,如果需要在不同批次的 execute
方法之间保持状态,可以实现 Database.Stateful
接口,类中的成员变量值将在所有批次执行之间被保留。
Scheduled Apex (Schedulable 接口)
Scheduled Apex 用于在特定的时间点自动执行 Apex 代码。例如,在每天凌晨 2 点运行一个数据清理的批处理任务。
核心特点:
- 实现接口:需要实现
Schedulable
接口及其唯一的execute(context)
方法。 - 调度方式:可以通过 Salesforce UI 的“Apex Classes”设置页面手动调度,也可以通过执行
System.schedule(jobName, cronExpression, schedulableInstance)
方法以编程方式进行调度。
* CRON 表达式:调度时间由一个 CRON 表达式定义,它能让你灵活地指定任务执行的分钟、小时、日期、月份和星期。
示例代码
以下代码示例均来自 Salesforce 官方文档,旨在展示不同异步 Apex 的标准实现方式。
Future Method 示例:从触发器进行外部调用
这个例子展示了如何在一个 Account 触发器中,通过 Future 方法调用外部服务来更新联系人信息。
// Future 方法类,负责执行 callout public class FutureMethodExample { @future(callout=true) public static void updateContacts(List<Id> accountIds) { // 查询需要更新的联系人 List<Contact> contactsToUpdate = [SELECT Id, AccountId, Name FROM Contact WHERE AccountId IN :accountIds]; // 模拟调用外部服务的请求构建 // 在实际场景中,这里会构建一个 HTTP 请求 String requestBody = '...'; // 根据联系人信息构建请求体 // 模拟发送 HTTP 请求 Http http = new Http(); HttpRequest request = new HttpRequest(); request.setEndpoint('https://api.example.com/update_contacts'); request.setMethod('POST'); request.setHeader('Content-Type', 'application/json;charset=UTF-8'); request.setBody(requestBody); // 发送请求并获取响应 // HttpResponse response = http.send(request); // 模拟处理响应 // if (response.getStatusCode() == 200) { // System.debug('Callout successful!'); // } else { // System.debug('Callout failed: ' + response.getBody()); // } } } // Account 触发器,在 Account 更新后调用 Future 方法 trigger AccountTrigger on Account (after update) { Set<Id> accountIds = Trigger.newMap.keySet(); // 调用 future 方法,将 DML 操作与 callout 分离 FutureMethodExample.updateContacts(new List<Id>(accountIds)); }
Queueable Apex 示例:任务链
这个例子展示了如何创建一个 Queueable 任务来处理商机,并在完成后链式启动第二个任务。
// 第一个 Queueable 任务,处理父记录 public class FirstQueueableJob implements Queueable { private List<Opportunity> opportunities; public FirstQueueableJob(List<Opportunity> records) { this.opportunities = records; } public void execute(QueueableContext context) { // 在这里执行对商机的复杂处理 for(Opportunity opp : opportunities) { opp.Description = 'Processed by First Job on ' + System.now(); } update opportunities; // 处理完成后,启动第二个任务 // 假设 SecondQueueableJob 是另一个实现了 Queueable 接口的类 System.enqueueJob(new SecondQueueableJob()); } } // 第二个 Queueable 任务 public class SecondQueueableJob implements Queueable { public void execute(QueueableContext context) { // 执行一些后续清理或通知任务 System.debug('Second job executed successfully.'); } }
Batch Apex 示例:批量更新客户记录
这是一个经典的 Batch Apex 示例,它会查找所有活跃的客户并将他们的描述字段更新。
public class UpdateAccountDescriptionBatch implements Database.Batchable<sObject>, Database.Stateful { // 成员变量用于在批次间保持状态 public Integer recordsProcessed = 0; // start 方法:收集需要处理的记录 public Database.QueryLocator start(Database.BatchableContext bc) { // 返回一个 QueryLocator,它包含了所有需要处理的客户记录 return Database.getQueryLocator( 'SELECT Id, Name, Description FROM Account WHERE Active__c = \'Yes\'' ); } // execute 方法:处理每个批次的数据 public void execute(Database.BatchableContext bc, List<Account> scope) { // 'scope' 是一个记录列表,代表当前批次 for (Account acc : scope) { acc.Description = 'Updated by Batch Apex on ' + System.today(); } update scope; // 因为实现了 Database.Stateful,可以更新成员变量 recordsProcessed = recordsProcessed + scope.size(); } // finish 方法:所有批次完成后执行 public void finish(Database.BatchableContext bc) { // 任务完成后,可以发送一封邮件通知 AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems, CreatedBy.Email FROM AsyncApexJob WHERE Id = :bc.getJobId()]; // 示例:发送邮件通知任务创建者 Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); String[] toAddresses = new String[] {job.CreatedBy.Email}; mail.setToAddresses(toAddresses); mail.setSubject('Batch Job: Update Account Description Status: ' + job.Status); mail.setPlainTextBody( 'The batch Apex job processed ' + job.TotalJobItems + ' batches with '+ job.NumberOfErrors + ' failures.\n' + 'Total records processed: ' + recordsProcessed ); Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail }); } }
注意事项
在使用异步 Apex 时,必须时刻关注平台的限制和一些关键的设计要点,以确保应用程序的稳定性和性能。
Governor 限制
异步事务虽然有比同步事务更高的限制(例如 CPU 时间为 60 秒,堆大小为 12MB),但它们并非没有限制。特别需要关注的是 24 小时内的异步执行次数限制,例如 Future 方法和 Queueable 任务共享 250,000 次或用户许可证数乘以 200 的较大值。过度滥用异步调用会导致组织在一段时间内无法处理任何后台任务。
执行顺序和时间
异步任务的执行时间是不确定的。当一个任务被入队后,它会等待系统资源可用时才被执行。因此,不能假设一个异步任务会立即或在特定时间内完成。同样,也不能保证任务会按照它们被提交的顺序执行,除非你使用了 Queueable 任务链来明确控制顺序。
错误处理
由于异步任务在后台运行,错误不会直接反馈给用户。因此,必须在代码中实现完善的错误处理机制。在 `execute` 方法中使用 `try-catch` 块来捕获异常,并将错误信息记录到自定义对象或平台事件 (Platform Events) 中,以便于后续的排查和处理。对于 Batch Apex,可以考虑实现 Database.RaisesPlatformEvents
接口来自动化地发布批处理错误事件。
代码测试
测试异步 Apex 代码需要使用 `Test.startTest()` 和 `Test.stopTest()` 方法。将异步方法的调用放在这两个方法之间。当 `Test.stopTest()` 执行时,Salesforce 会立即同步执行所有已入队的异步任务,这样你就可以在测试代码的后续部分通过断言 (assertions) 来验证异步任务的执行结果是否符合预期。
总结与最佳实践
Asynchronous Apex 是 Salesforce 平台提供的一套强大工具,是每一位开发人员都必须熟练掌握的核心技能。它让我们能够突破同步事务的限制,构建能够处理复杂业务逻辑和海量数据的应用程序。
如何选择合适的异步工具?
- Future 方法:当你需要一个简单、快速的方案来隔离 DML 操作和外部调用,或者执行一个独立的、不需要监控的后台任务时,Future 方法是最佳选择。
- Queueable Apex:当你需要传递 sObject 等复杂数据类型、需要监控任务状态、或者需要将多个异步任务链接成一个处理流程时,Queueable Apex 是更现代、更强大的选择。在大多数场景下,它都可以替代 Future 方法。
- Batch Apex:当你的核心需求是处理成千上万条记录时,Batch Apex 是唯一的正确答案。它的分块处理机制能够保证在不超出 Governor 限制的情况下完成大数据量的操作。
- Scheduled Apex:当你需要让任务在未来某个特定时间或按固定周期自动运行时,使用 Scheduled Apex。通常,Scheduled Apex 的 `execute` 方法会去调用一个 Batch Apex 或 Queueable Apex 类来执行实际的业务逻辑。
最佳实践:
- 保持代码的幂等性 (Idempotent):异步任务可能会因为某些原因(如平台维护)而重试。确保你的代码逻辑即使重复执行一次,也不会产生错误的业务结果。
- 代码要“笨重化”:尽量将大量的处理逻辑集中在异步代码中,而让触发器等同步代码保持轻量,只负责分发任务。这被称为 “Trigger Framework” 的一种设计模式。
- 不要在循环中调用异步方法:绝对避免在 for 或 while 循环中调用 `System.enqueueJob` 或 Future 方法,这会迅速耗尽你的异步执行限制。应该将需要处理的数据收集到一个集合中,然后一次性地将整个集合传递给一个异步方法。
- 监控 `AsyncApexJob`:定期监控 `AsyncApexJob` 对象,检查是否有失败的任务,及时发现并解决问题。
总而言之,深入理解并合理运用这四种异步 Apex 机制,将使你能够构建出既能满足复杂业务需求,又具备高可扩展性和高用户体验的 Salesforce 解决方案。
评论
发表评论