Salesforce 异步 Apex 深度解析:开发者指南
背景与应用场景
作为一名 Salesforce 开发人员,我们日常工作中不可避免地会遇到 Salesforce 平台的 Governor Limits (治理限制)。这些限制是为了保证在多租户环境下,所有组织都能公平地共享资源。然而,在处理复杂业务逻辑、大量数据或与外部系统集成时,同步执行的 Apex 代码很容易触碰到这些天花板,例如:单个事务中的 SOQL 查询总数(100个)、DML 语句总数(150个)以及 CPU 总执行时间(10秒)等。
当用户操作触发的逻辑需要花费很长时间才能完成时,同步处理会严重影响用户体验,导致页面长时间无响应。为了解决这些问题,Salesforce 提供了强大的异步处理框架——Asynchronous Apex。它允许我们将那些耗时、资源密集型的任务放到后台执行,从而释放主线程,优化性能并提升用户体验。
典型的应用场景包括:
- 处理海量数据:当需要对成千上万条记录进行更新、删除或复杂计算时,同步处理几乎是不可能的。例如,在月底对所有客户的积分进行批量结算。
- 外部系统调用 (Callouts):从 Apex Trigger 或其他同步上下文中直接调用外部 Web 服务是被禁止的,因为这可能导致事务长时间挂起。异步 Apex 是执行这些调用的标准方式。
- 复杂的计算逻辑:某些业务逻辑,如复杂的定价计算、风险评估模型,其计算量可能超出同步 CPU 时间限制,需要移至后台处理。
- 提升用户体验:对于用户触发但不需要立即看到结果的操作(如生成一份复杂的年度报告),可以将其放入后台队列,并立即向用户返回“任务已提交”的反馈,而不是让用户在屏幕前苦等。
原理说明
Asynchronous Apex 的核心原理是将任务提交到一个后台处理队列中。Salesforce 平台会根据可用资源,在适当的时候从队列中取出任务并执行。这些任务在自己独立的事务中运行,拥有更高、更独立的 Governor Limits。Salesforce 主要提供了四种异步 Apex 的实现方式,每种都有其特定的适用场景和优缺点。
Future Methods (@future)
Future Methods 是最简单的一种异步 Apex。通过在方法上添加 @future 注解,我们可以告诉平台这个方法应该在后台异步执行。它非常适合那些“即发即忘”(fire-and-forget) 的简单后台任务。
核心特点:
- 简单易用:只需一个注解即可实现异步。
- 参数限制:方法必须是静态的 (static),返回类型必须是 void,并且其参数只能是基本数据类型 (primitives)、基本数据类型的数组或集合。不支持传递 sObject 对象作为参数,但可以传递 sObject 的 ID,然后在方法内部查询。
- 独立事务:每个 future 方法都在自己的事务中执行。
- Callouts:非常适合用于从触发器等同步代码中发起 Web 服务调用,只需添加
@future(callout=true)注解。 - 无序执行:Future 方法的执行顺序是不保证的。如果你提交了多个 future 任务,它们可能不会按照你提交的顺序执行。
Batch Apex (Database.Batchable)
Batch Apex 是专为处理大量数据而设计的。它将一个大的数据处理任务分割成多个小的、可管理的“块”(chunks),然后分批次地异步处理。这使得我们能够处理数百万条记录,而不会超出 Governor Limits。
核心特点:
一个 Batch Apex 类必须实现 Database.Batchable 接口,该接口包含三个方法:
start(Database.BatchableContext bc):这是批处理的起点。它负责收集需要处理的所有记录,并返回一个Database.QueryLocator或一个Iterable对象。对于海量数据,强烈推荐使用Database.QueryLocator,因为它支持高达 5000 万条记录。execute(Database.BatchableContext bc, List<SObject> scope):这是核心处理逻辑所在。平台会多次调用此方法,每次传入一小批数据(即scope参数,默认大小为 200)。你所有的业务逻辑,如字段更新、DML 操作,都在这里完成。finish(Database.BatchableContext bc):当所有批次都处理完毕后,此方法会被调用一次。通常用于执行一些总结性的操作,比如发送一封通知邮件或启动另一个后续任务。
如果需要在不同 execute 调用之间保持状态(例如,统计总共处理了多少条记录),可以额外实现 Database.Stateful 接口。
Queueable Apex (Queueable)
Queueable Apex 可以看作是 Future Methods 的升级版。它提供了更强大的功能和灵活性,克服了 Future Methods 的一些主要限制。
核心特点:
- 复杂参数类型:与 Future Methods 不同,Queueable Apex 的类成员变量可以是非基本数据类型,例如 sObject 甚至自定义的 Apex 类对象。
- 获取 Job ID:当你通过
System.enqueueJob(new MyQueueableClass())将一个 Queueable 任务入队时,会返回一个 Job ID。你可以使用这个 ID 来监控任务的状态(通过查询AsyncApexJob对象)。 - 任务链 (Job Chaining):一个 Queueable 任务可以启动另一个 Queueable 任务。这使得构建一系列按顺序执行的复杂异步流程成为可能,这是 Future Methods 无法做到的。
它通过实现 Queueable 接口及其唯一的 execute(QueueableContext context) 方法来工作。
Scheduled Apex (Schedulable)
Scheduled Apex 顾名思义,用于在指定的时间或按固定的周期重复执行任务。例如,每天凌晨 1 点执行数据清理,或每周一早上生成周报数据。
核心特点:
- 定时执行:可以精确控制代码的执行时间。
- 实现简单:只需实现
Schedulable接口及其唯一的execute(SchedulableContext context)方法。 - 调度方式:可以通过 Salesforce UI(在“设置”中搜索“Apex 类”)进行可视化调度,也可以通过
System.schedule方法以编程方式动态创建调度任务。
示例代码(含详细注释)
以下所有代码示例均严格遵循 Salesforce 官方文档。
Future Method 示例:从触发器进行外部调用
这是一个典型的场景,当一个客户记录被创建或更新时,需要异步调用外部服务来同步信息。
public class FutureMethodExample {
@future(callout=true)
public static void sendAccountInfo(Id accountId) {
// 根据传入的 Account ID 查询最新的数据
Account acct = [SELECT Id, Name, Phone FROM Account WHERE Id = :accountId];
// 构造发送到外部服务的数据
String jsonBody = '{"name":"' + acct.Name + '", "phone":"' + acct.Phone + '"}';
// 设置 HTTP 请求
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/accounts');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json;charset=UTF-8');
req.setBody(jsonBody);
// 发送请求并处理响应
Http http = new Http();
try {
HttpResponse res = http.send(req);
System.debug('Callout successful. Status code: ' + res.getStatusCode());
// 在这里可以添加对响应的处理逻辑
} catch (System.CalloutException e) {
System.debug('Callout error: ' + e.getMessage());
// 异常处理逻辑
}
}
}
// 在 Account 触发器中调用
trigger AccountTrigger on Account (after insert, after update) {
for (Account a : Trigger.new) {
// 将需要异步处理的 Account ID 传递给 future 方法
FutureMethodExample.sendAccountInfo(a.Id);
}
}
Batch Apex 示例:批量更新联系人地址
假设我们需要将所有在“北京”的联系人的邮寄地址统一更新为公司的某个新地址。
public class UpdateContactAddresses implements Database.Batchable<sObject>, Database.Stateful {
// 使用 Database.Stateful 来跟踪已处理的记录数
private integer recordsProcessed = 0;
// 1. start 方法:收集需要处理的数据
public Database.QueryLocator start(Database.BatchableContext bc) {
// 返回一个 QueryLocator,查询所有在北京的联系人
return Database.getQueryLocator(
'SELECT Id, MailingStreet FROM Contact WHERE MailingCity = \'北京\''
);
}
// 2. execute 方法:处理每一批数据
public void execute(Database.BatchableContext bc, List<Contact> scope) {
// 创建一个列表来存储待更新的联系人
List<Contact> contactsToUpdate = new List<Contact>();
for (Contact contact : scope) {
// 更新业务逻辑
contact.MailingStreet = '北京市海淀区中关村大街1号';
contactsToUpdate.add(contact);
}
// 对当前批次执行一次 DML 操作
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
// 更新已处理记录的计数器
recordsProcessed = recordsProcessed + contactsToUpdate.size();
}
}
// 3. 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('联系人地址批量更新任务完成 - ' + job.Status);
mail.setPlainTextBody(
'批处理任务已处理 ' + job.TotalJobItems +
' 条记录,其中失败 ' + job.NumberOfErrors + ' 条。\n' +
'我们通过代码逻辑统计,共更新了 ' + recordsProcessed + ' 条记录。'
);
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}
// 如何启动这个 Batch Job
// Id batchJobId = Database.executeBatch(new UpdateContactAddresses(), 100);
// 第二个参数 100 是可选的 scope size,指定了每个 execute 方法处理的记录数
Queueable Apex 示例:任务链
这个例子展示了如何创建一个任务,处理完后链接到另一个任务。
public class FirstQueueableJob implements Queueable {
public void execute(QueueableContext context) {
// 步骤1: 查询一个 Account 记录
Account a = [SELECT Id, Name, AnnualRevenue FROM Account WHERE Name = 'ACME' LIMIT 1];
// 步骤2: 执行一些业务逻辑
// ... 例如更新 Account 的某个字段
System.debug('正在处理第一个任务,Account: ' + a.Name);
// 步骤3: 链式调用第二个 Queueable 任务
// 你可以将当前任务处理的结果(如 sObject)传递给下一个任务
System.enqueueJob(new SecondQueueableJob(a));
}
}
public class SecondQueueableJob implements Queueable {
private Account processedAccount;
// 构造函数,接收从第一个任务传递过来的数据
public SecondQueueableJob(Account acc) {
this.processedAccount = acc;
}
public void execute(QueueableContext context) {
// 步骤4: 基于上一个任务的结果,执行后续逻辑
System.debug('正在处理第二个任务,接收到的 Account ID: ' + processedAccount.Id);
// ... 例如,基于这个 Account 创建一个 Opportunity
}
}
// 如何启动这个链式任务
// System.enqueueJob(new FirstQueueableJob());
注意事项
治理限制 (Governor Limits)
虽然异步 Apex 的限制比同步的要宽松得多,但它并非没有限制。你需要关注每日异步 Apex 执行次数(250,000次或用户许可证数乘以200,取较大者)、并发执行的批处理作业数量等。务必在设计时考虑到这些限制。
测试异步 Apex
测试异步代码是开发中的一个关键环节。你必须将你的异步调用代码块包裹在 Test.startTest() 和 Test.stopTest() 之间。Test.stopTest() 会强制所有在 startTest() 之后调用的异步作业立即执行完毕,这样你就可以在测试方法的后续部分对异步执行的结果进行断言 (assert)。
@isTest
private class MyAsyncTest {
static testMethod void testQueueable() {
// 准备测试数据
Account testAcc = new Account(Name='ACME');
insert testAcc;
Test.startTest();
// 将 Queueable 作业入队
System.enqueueJob(new FirstQueueableJob());
Test.stopTest(); // stopTest() 会强制执行上面的异步代码
// 在这里查询并断言异步代码执行的结果
// 例如,验证 SecondQueueableJob 是否成功创建了 Opportunity
}
}
错误处理
异步任务在后台运行,如果发生异常,用户无法立即感知。因此,在代码中实现健全的 try-catch 机制至关重要。你应当捕获异常,并将其记录到自定义对象、发送平台事件或通知相关人员,以便进行问题排查。
幂等性 (Idempotency)
在设计异步流程时,特别是可能因错误而重试的流程,要考虑幂等性。即一个操作执行一次和执行多次的效果应该是一样的。例如,避免使用 `i++` 这样的操作,而是使用 `UPDATE Account SET Status = 'Processed'` 这样的确定性操作。
总结与最佳实践
Asynchronous Apex 是 Salesforce 平台开发人员工具箱中不可或缺的一部分。掌握它,你才能构建出真正可扩展、高性能且用户体验良好的复杂应用程序。
如何选择?
- Future Methods (`@future`):当你需要一个简单、快速的方案来从同步代码(如触发器)中执行一个独立的、耗时的任务(特别是外部调用)时使用。
- Batch Apex (`Database.Batchable`):当你的核心需求是处理成千上万甚至数百万条记录时,这是唯一的选择。它是为海量数据处理而生的。
- Queueable Apex (`Queueable`):当你需要比 Future Method 更多的控制权时,例如需要监控任务状态、传递复杂对象或将多个异步任务链接在一起时,这是现代化的首选。
- Scheduled Apex (`Schedulable`):当你需要让代码在未来的特定时间点或按固定周期自动运行时使用。
最佳实践:
- 选择正确的工具:不要用 Batch Apex 去处理几十条数据,也不要试图用 Future Method 实现复杂的多步流程。
- 代码要健壮:始终包含详尽的错误处理和日志记录机制。
- 保持批量化思维:即使在 Batch Apex 的
execute方法内部,也要遵循 Apex 的批量化最佳实践,避免在循环中执行 SOQL 或 DML。 - 监控你的队列:定期检查“设置”中的“Apex 作业”页面,了解异步任务的执行情况,及时发现失败或性能问题。
- 注意递归和链式深度:在使用 Queueable 任务链时,要小心设计退出条件,避免无限递归,同时注意链式调用的深度限制。
通过合理地运用这四种异步模式,你将能够突破同步限制,构建出能够应对各种复杂业务场景的强大 Salesforce 应用。
评论
发表评论