掌握异步 Apex:提升 Salesforce 性能与可扩展性
背景与应用场景
作为一名 Salesforce 开发人员,我们经常面临一个核心挑战:如何在 Salesforce 平台严格的 Governor Limits(系统限制)下,处理大量数据、执行长时间运行的业务逻辑或与外部系统进行复杂的集成。同步执行的 Apex 代码(Synchronous Apex)受到严格的执行时间、DML 操作、SOQL 查询等限制,这使得它不适合处理那些可能耗时过长或资源密集型的任务。
为了解决这些问题,Salesforce 提供了强大的 Asynchronous Apex(异步 Apex)机制。异步 Apex 允许代码在独立的线程中,以后台进程的形式运行,并且拥有自己的、更为宽松的 Governor Limits,从而避免了阻塞用户界面或因超时而失败的风险。这极大地提升了应用程序的性能、可扩展性和用户体验。
以下是异步 Apex 常见的应用场景:
- 批量数据处理:当需要处理成千上万甚至数百万条记录时(例如,数据迁移、定期数据清理、批量更新字段),同步 Apex 几乎不可能完成,而 Batch Apex 则是理想选择。
- 外部系统集成(Callouts):与外部 Web 服务进行 HTTP 请求(Callouts)通常需要时间,如果同步执行会阻塞用户界面。使用 Future 方法或 Queueable Apex 进行异步 Callouts 可以避免此问题。
- 长时间运行的计算或业务逻辑:例如,复杂的财务计算、数据聚合、报告生成,这些任务可能会超出同步 Apex 的 CPU 时间限制。
- 定时任务:需要定期运行的代码(例如,每日数据同步、每周报告生成、每月数据归档),可以通过 Scheduled Apex 实现自动化。
- 队列化操作:当需要在一个事务完成后触发另一个异步操作,或者需要将复杂的数据结构传递给异步方法时,Queueable Apex 提供了比 Future 方法更灵活的解决方案。
Salesforce 提供了四种主要的异步 Apex 类型:
- Future 方法 (Future Methods):最简单的异步执行方式。
- Batch Apex (批量 Apex):专门设计用于处理大量记录。
- Queueable Apex (队列化 Apex):比 Future 方法更灵活,支持链式调用和复杂数据类型。
- Scheduled Apex (计划 Apex):用于在特定时间运行 Apex 代码。
原理说明
异步 Apex 的核心原理在于它将任务提交到一个后台队列中,由 Salesforce 平台在资源可用时进行处理。每个异步作业都在独立的事务中运行,这意味着它们有自己独立的 Governor Limits,并且不会影响提交它们的原始事务。这种分离确保了即使异步任务失败,也不会回滚触发它的主事务。
Future 方法
Future 方法使用 @future 注解标记,它是一个静态方法,并且只能接受原始数据类型(primitive data types)、Map 和 List 的非 SObject 类型。它不能接受 SObject 或 SObject 的集合作为参数。如果方法需要执行 Callout,则需要额外指定 (callout=true)。Future 方法是“即发即忘”(fire-and-forget)类型,不支持作业链,也无法轻松跟踪其状态。
Batch Apex
Batch Apex 用于处理大量记录,可以将查询结果集分解成多个批次(batches),每个批次在一个独立的事务中处理。它实现了 Database.Batchable 接口,并包含三个核心方法:
start(Database.BatchableContext bc):在批处理作业开始时调用,通常用于查询并返回要处理的记录。它返回一个Database.QueryLocator或一个Iterable。execute(Database.BatchableContext bc, List:对每个批次的记录进行实际处理。scope) scope参数包含了当前批次的记录列表。finish(Database.BatchableContext bc):在所有批次处理完成后调用,通常用于发送通知邮件或执行后续操作。
每个 execute 方法的调用都算作一个独立的事务,拥有自己的 Governor Limits。
Queueable Apex
Queueable Apex 实现了 Queueable 接口,并包含一个 execute(QueueableContext context) 方法。它提供了一些 Future 方法不具备的优势:
- 传递 SObject 类型参数:可以直接将 SObject 类型的参数传递给 Queueable 类。
- 作业链 (Chaining Jobs):可以在一个 Queueable 作业的
execute方法中调用另一个 Queueable 作业,从而创建一系列的异步任务。 - 跟踪作业状态:可以获取到提交作业的 ID,从而在
AsyncApexJob对象中跟踪其状态。
它在一定程度上可以看作是 Future 方法和 Batch Apex 的结合体,适用于需要更复杂参数传递和作业链的场景。
Scheduled Apex
Scheduled Apex 实现了 Schedulable 接口,并包含一个 execute(SchedulableContext sc) 方法。它允许我们使用 System.schedule 方法,通过 CRON 表达式 来指定代码的运行时间。Scheduled Apex 可以用来运行 Batch Apex 或 Queueable Apex,使其定期执行。
示例代码
以下是一些异步 Apex 类型的代码示例,均基于 Salesforce 官方文档提供的方法和结构。
Future 方法示例
此示例展示了一个简单的 Future 方法,用于在后台更新一个账户的描述,并模拟了一个外部 Callout。
public class FutureMethods {
// 示例1: 更新账户描述 - 不涉及Callout
@future
public static void updateAccountDescription(Id accountId, String newDescription) {
try {
Account acc = [SELECT Id, Description FROM Account WHERE Id = :accountId LIMIT 1];
if (acc != null) {
acc.Description = newDescription + ' (Updated by Future Method)';
update acc;
System.debug('Account ' + accountId + ' description updated successfully by future method.');
}
} catch (Exception e) {
System.debug('Error updating account description: ' + e.getMessage());
}
}
// 示例2: 模拟Callout到外部服务
// 注意: 真正的Callout需要配置Remote Site Settings
@future(callout=true)
public static void makeHttpCallout(Id accountId) {
try {
// 模拟一个HTTP请求
HttpRequest req = new HttpRequest();
req.setEndpoint('https://www.someexternalapi.com/data/' + accountId); // 替换为实际的外部API端点
req.setMethod('GET');
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
System.debug('Callout successful for account ' + accountId + '. Response: ' + res.getBody());
// 在此处可以解析响应并更新Salesforce记录
} else {
System.debug('Callout failed for account ' + accountId + '. Status: ' + res.getStatusCode() + ', Body: ' + res.getBody());
}
} catch (Exception e) {
System.debug('Error making HTTP callout: ' + e.getMessage());
}
}
// 调用Future方法
public static void invokeFutureMethods() {
// 创建一个测试账户
Account testAcc = new Account(Name = 'Test Account for Future', Description = 'Original description');
insert testAcc;
// 调用更新账户描述的Future方法
FutureMethods.updateAccountDescription(testAcc.Id, 'New description from async process');
// 调用模拟HTTP Callout的Future方法
// 注意: 真实环境中,需要在Setup -> Remote Site Settings中添加'https://www.someexternalapi.com'
FutureMethods.makeHttpCallout(testAcc.Id);
}
}
Batch Apex 示例
此示例展示了一个 Batch Apex 类,用于批量更新所有账户的描述,为其添加一个后缀。
public class AccountBatchUpdate implements Database.Batchable, Database.Stateful { // 用于存储处理的账户数量,Database.Stateful 接口允许在 execute 方法之间保持实例状态 public Integer recordsProcessed = 0; // start 方法:查询要处理的记录 public Database.QueryLocator start(Database.BatchableContext bc) { System.debug('Batch Start method executed.'); // 返回所有账户记录 return Database.getQueryLocator('SELECT Id, Name, Description FROM Account'); } // execute 方法:处理每个批次的记录 public void execute(Database.BatchableContext bc, List scope) { System.debug('Batch Execute method started for ' + scope.size() + ' records.'); List accountsToUpdate = new List (); for (Account acc : scope) { // 避免重复更新,确保幂等性 if (acc.Description == null || !acc.Description.contains(' (Batch Updated)')) { acc.Description = (acc.Description == null ? '' : acc.Description) + ' (Batch Updated)'; accountsToUpdate.add(acc); recordsProcessed++; } } if (!accountsToUpdate.isEmpty()) { update accountsToUpdate; System.debug('Updated ' + accountsToUpdate.size() + ' accounts in this batch.'); } } // finish 方法:所有批次处理完成后执行 public void finish(Database.BatchableContext bc) { System.debug('Batch Finish method executed. Total records processed: ' + recordsProcessed); // 可以发送邮件通知或执行后续清理工作 AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems, CreatedBy.Email FROM AsyncApexJob WHERE Id = :bc.getJobId()]; System.debug('Batch job ' + job.Id + ' finished with status ' + job.Status + '. Errors: ' + job.NumberOfErrors); // 发送通知邮件 Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage(); String[] toAddresses = new String[] {job.CreatedBy.Email}; // 发送给启动批处理的用户 mail.setToAddresses(toAddresses); mail.setSubject('Account Batch Update Finished: ' + job.Status); mail.setPlainTextBody( 'The batch job to update account descriptions has finished.\n' + 'Total records processed: ' + recordsProcessed + '\n' + 'Job ID: ' + job.Id + '\n' + 'Status: ' + job.Status + '\n' + 'Errors: ' + job.NumberOfErrors ); Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail }); } // 如何调用Batch Apex public static Id runBatch() { AccountBatchUpdate batchJob = new AccountBatchUpdate(); return Database.executeBatch(batchJob, 200); // 每次处理200条记录 } }
Queueable Apex 示例
此示例展示了一个 Queueable Apex 类,用于在后台处理一个联系人列表,并演示了如何进行作业链。
public class ProcessContactsQueueable implements Queueable {
private List contactIds;
private Boolean chainNextJob;
public ProcessContactsQueueable(List ids, Boolean chainNext) {
this.contactIds = ids;
this.chainNextJob = chainNext;
}
public void execute(QueueableContext context) {
System.debug('Queueable job started for ' + contactIds.size() + ' contacts.');
List contactsToUpdate = [SELECT Id, Description FROM Contact WHERE Id IN :contactIds];
List updatedContacts = new List();
for (Contact con : contactsToUpdate) {
con.Description = (con.Description == null ? '' : con.Description) + ' (Processed by Queueable)';
updatedContacts.add(con);
}
if (!updatedContacts.isEmpty()) {
update updatedContacts;
System.debug('Updated ' + updatedContacts.size() + ' contacts.');
}
// 示例: 链式调用另一个Queueable作业
if (chainNextJob && !updatedContacts.isEmpty()) {
List nextBatchIds = new List();
// 假设我们只处理第一个被更新联系人的父账户
if(updatedContacts[0].AccountId != null) {
nextBatchIds.add(updatedContacts[0].AccountId);
}
if (!nextBatchIds.isEmpty()) {
System.debug('Chaining to next Queueable job to process account: ' + nextBatchIds[0]);
System.enqueueJob(new ProcessAccountsQueueable(nextBatchIds));
}
}
}
// 另一个 Queueable 类,用于演示链式调用
public class ProcessAccountsQueueable implements Queueable {
private List accountIds;
public ProcessAccountsQueueable(List ids) {
this.accountIds = ids;
}
public void execute(QueueableContext context) {
System.debug('Chained Queueable job (ProcessAccountsQueueable) started for ' + accountIds.size() + ' accounts.');
List accountsToUpdate = [SELECT Id, BillingCity FROM Account WHERE Id IN :accountIds];
for (Account acc : accountsToUpdate) {
if (acc.BillingCity == null) {
acc.BillingCity = 'Default City';
}
}
if (!accountsToUpdate.isEmpty()) {
update accountsToUpdate;
System.debug('Updated ' + accountsToUpdate.size() + ' accounts from chained job.');
}
}
}
// 如何调用Queueable Apex
public static Id enqueueJob() {
// 创建一些测试联系人
List testContacts = new List();
for (Integer i = 0; i < 5; i++) {
testContacts.add(new Contact(FirstName = 'Test', LastName = 'Contact ' + i, Description = 'Initial Description'));
}
insert testContacts;
List contactIdsToProcess = new List();
for (Contact c : testContacts) {
contactIdsToProcess.add(c.Id);
}
// 提交Queueable作业,并指示是否链式调用
return System.enqueueJob(new ProcessContactsQueueable(contactIdsToProcess, true));
}
}
Scheduled Apex 示例
此示例展示了一个 Scheduled Apex 类,用于每天凌晨运行一个清理任务。
public class DailyCleanupScheduler implements Schedulable {
public void execute(SchedulableContext sc) {
System.debug('DailyCleanupScheduler job started. Job Name: ' + sc.getJobId());
// 可以在这里调用其他异步作业,例如 Batch Apex 或 Queueable Apex
// 例如,启动一个批量删除旧任务的 Batch Apex
// Database.executeBatch(new DeleteOldTasksBatch(), 200);
// 也可以直接执行DML操作或SOQL查询
List oldTasks = [SELECT Id FROM Task WHERE CreatedDate < LAST_N_DAYS:90];
if (!oldTasks.isEmpty()) {
delete oldTasks;
System.debug('Deleted ' + oldTasks.size() + ' tasks older than 90 days.');
} else {
System.debug('No old tasks found to delete.');
}
}
// 如何调度Scheduled Apex
public static String scheduleDailyJob() {
// CRON 表达式: 秒 分 时 日 月 星期几 (年 - 可选)
// '0 0 0 * * ?' 表示每天凌晨 00:00:00 运行
// '0 30 1 * * ?' 表示每天凌晨 01:30:00 运行
String cronExp = '0 0 0 * * ?'; // 每天午夜运行
DailyCleanupScheduler dailyJob = new DailyCleanupScheduler();
String jobId = System.schedule('Daily Cleanup Job', cronExp, dailyJob);
System.debug('Scheduled Daily Cleanup Job with ID: ' + jobId);
return jobId;
}
}
注意事项
在使用异步 Apex 时,作为开发人员,需要特别关注以下几个方面:
Governor Limits
虽然异步 Apex 拥有更宽松的限制,但它们并非无限。每种异步类型都有其特定的限制:
- Future 方法:每个事务最多可以调用 50 个 Future 方法。
- Batch Apex:
- 单个 `execute` 方法中查询的记录数:
Database.QueryLocator最多可以处理 5000 万条记录,Iterable则最多 5 万条。 - 每个 `execute` 方法都有独立的 Governor Limits(例如,10000 条 DML 行、100 个 SOQL 查询等)。
- 同时运行的批处理作业数量:最多 5 个。
- 单个 `execute` 方法中查询的记录数:
- Queueable Apex:
- 每个事务最多可以入队 50 个 Queueable 作业。
- 可以通过
System.enqueueJob链式调用多达 5 层(一个作业可以再调用一个,依此类推,最多 5 个)。
- Scheduled Apex:
- 最多可以有 100 个 Scheduled Apex 作业处于“已计划”状态。
- Scheduled Apex 调度后,它会触发一次 `execute` 方法的运行,该运行也受异步 Governor Limits 约束。
务必查阅 Apex Governor Limits 官方文档以获取最准确和最新的信息。
错误处理与监控
- Try-Catch 块:在异步方法中,始终使用
try-catch块来捕获潜在的异常。这可以防止作业完全失败,并允许你记录错误信息。 - 日志记录:使用
System.debug()进行详细的日志记录,以便在沙箱环境中调试。在生产环境中,可以考虑将错误信息记录到自定义对象中,以便于监控和报告。 - 邮件通知:在 Batch Apex 的
finish方法或 Queueable Apex 的execute方法(如果发生严重错误)中发送邮件通知给管理员,以便及时了解作业状态和错误。 - 监控:通过 Salesforce UI 中的 Setup (设置) -> Apex Jobs (Apex 作业) 或通过查询
AsyncApexJob对象来监控异步作业的状态。
幂等性 (Idempotency)
设计异步作业时,应尽可能使其具有幂等性。这意味着即使同一个作业被多次运行,其结果也应保持一致,不会产生不必要的副作用(例如,重复创建记录或重复更新数据)。在更新记录时,可以先检查记录的当前状态,避免不必要的 DML 操作。
测试异步代码
异步 Apex 代码必须通过单元测试,并且需要使用 Test.startTest() 和 Test.stopTest() 方法。所有在 Test.startTest() 和 Test.stopTest() 之间提交的异步作业,都会在 Test.stopTest() 执行后立即同步运行,确保测试能够覆盖异步逻辑。
@isTest
private class FutureMethodsTest {
@isTest static void testUpdateAccountDescription() {
Account testAcc = new Account(Name = 'Test Acc For Future');
insert testAcc;
Test.startTest();
FutureMethods.updateAccountDescription(testAcc.Id, 'New Future Desc');
Test.stopTest();
// 验证异步操作是否成功
Account updatedAcc = [SELECT Id, Description FROM Account WHERE Id = :testAcc.Id];
System.assertEquals('New Future Desc (Updated by Future Method)', updatedAcc.Description);
}
// ... 其他异步方法的测试
}
共享与安全
异步 Apex (Future, Batch, Queueable, Scheduled) 默认在“系统模式”(System Mode)下运行,这意味着它们会忽略当前用户的共享设置(sharing settings)和字段级安全性(field-level security)。如果需要考虑用户权限,开发者必须在代码中显式实现权限检查,或者对 Batch Apex 使用 `with sharing` 关键字。通常情况下,对于后台数据处理,系统模式是期望的行为,但对于涉及敏感数据或用户特定视图的逻辑,需要特别注意。
请注意:虽然 `Batch Apex` 可以声明 `with sharing`,但 `QueryLocator` 在 `start` 方法中执行的查询仍会在系统模式下运行。`execute` 方法中的 DML 操作会遵循 `with sharing` 的权限设置。
链式调用限制
Queueable Apex 允许链式调用,但最多只能链式调用 5 层。过度链式调用可能会导致性能问题或达到其他 Governor Limits。
总结与最佳实践
异步 Apex 是 Salesforce 平台上处理复杂、长时间运行或大批量任务的基石。作为一名 Salesforce 开发人员,熟练掌握异步编程对于构建高性能、可扩展的应用程序至关重要。以下是一些总结和最佳实践:
- 选择合适的异步类型:
- Future 方法:适用于简单的“即发即忘”任务,如单个 Callout 或后台更新少数记录,不需跟踪状态。
- Batch Apex:处理大量记录(数万到数百万),需要对数据进行分批处理,并且需要跟踪作业进度。
- Queueable Apex:比 Future 方法更灵活,可传递 SObject 参数,支持作业链,并且可以跟踪作业 ID。适用于需要更精细控制的异步任务。
- Scheduled Apex:用于定期(例如每日、每周)执行代码,通常与其他异步类型结合使用(例如,调度一个 Batch Apex)。
- 最小化异步块中的工作量:虽然异步代码有更宽松的限制,但仍应尽量减少在这些方法中执行的工作量。只处理必要的逻辑,避免不必要的查询和计算,以提高效率。
- 健壮的错误处理:始终在异步代码中实现
try-catch块,并确保有适当的日志记录和通知机制(如邮件),以便在发生故障时及时响应。 - 设计幂等性操作:考虑到异步作业可能因系统重试或人为干预而多次运行,设计操作时应确保多次执行与单次执行产生相同的结果。
- 全面测试:使用
Test.startTest()和Test.stopTest()确保所有异步逻辑都被单元测试覆盖。 - 监控与优化:定期监控
Apex Jobs,关注失败的作业和性能瓶颈。根据监控结果,对异步逻辑进行优化。 - 避免硬编码 ID:在异步方法中避免硬编码记录 ID,而是通过参数传递或动态查询获取。
- 考虑替代方案:对于某些事件驱动的场景,可以考虑使用 Platform Events 或 Change Data Capture (CDC) 结合 Triggers 或 Flow 来实现更松散耦合的异步处理。
通过遵循这些最佳实践,您将能够有效地利用异步 Apex 来解决复杂的业务挑战,并为您的 Salesforce 应用程序提供卓越的性能和稳定性。
评论
发表评论