Salesforce 数据归档策略:架构师综合指南

身份:Salesforce 架构师


背景与应用场景

作为 Salesforce 架构师,我们设计和构建的系统不仅要满足眼前的业务需求,更要具备长期的可扩展性、高性能和成本效益。在 Salesforce 生态系统中,数据是核心资产,但随着业务的增长,数据量也会急剧膨胀。当一个组织的 Salesforce 实例面临 Large Data Volumes (LDV) (大数据量) 的挑战时,一系列问题便会接踵而至。

这些问题包括:

  • 性能下降:报告和仪表板加载缓慢,列表视图超时,SOQL 查询性能变差,甚至影响到整体用户体验。
  • 存储成本增加:Salesforce 的数据存储是有成本的。超出组织限制的存储空间需要额外购买,这会直接增加运营开销。
  • 治理与合规风险:许多行业(如金融、医疗)都有严格的数据保留 (Data Retention) 政策。无限期地保留所有数据不仅成本高昂,还可能违反如 GDPR、CCPA 等法规。
  • 敏捷性降低:在拥有海量数据的 Sandbox 中进行开发和测试会变得非常缓慢,部署和数据迁移等运维工作的复杂性和风险也随之增加。

因此,制定一个健全的数据归档 (Archiving) 策略,不再是一个可选项,而是维持 Salesforce 平台健康、高效和合规运行的必要措施。归档的核心思想是将那些不常访问但又因合规或业务原因需要保留的历史数据,从主要的、高性能的生产数据库中移出,迁移到一个成本更低、为长期存储而优化的位置。常见的归档场景包括:已关闭超过数年的客户服务个案 (Cases),已完成的活动 (Tasks),旧的销售机会 (Opportunities),或已离职员工的相关记录。

原理说明

设计归档策略时,架构师需要从多个维度进行权衡,包括数据访问频率、恢复需求、合规要求、技术能力和总体拥有成本 (Total Cost of Ownership, TCO)。没有一种“万能”的解决方案,正确的策略取决于具体的业务场景。以下是三种主流的归取策略,我将从架构师的视角分析其优劣。

策略一:原生平台归档 (On-Platform Archiving with Big Objects)

Salesforce 提供了 Big Objects 这一原生解决方案来处理海量数据。Big Objects 旨在 Salesforce 平台上提供可扩展、高性能的大数据存储能力。您可以将它们看作是组织内的“冷存储”或归档层。

工作原理: 我们通过自定义的 Batch Apex 或集成工具,定期查询生产环境中的标准或自定义对象(如 Case、Task),筛选出符合归档条件的旧记录。然后,将这些记录的数据转换并插入到预先定义好的 Big Object 中。插入成功后,再从源对象中删除这些记录,从而释放存储空间并提升性能。

优点:

  • 数据不出平台:数据保留在 Salesforce Trust 边界内,简化了安全和合规性审查。
  • 成本效益:对于特定数量级的记录,Big Objects 的成本通常低于增加标准数据存储。
  • 可访问性:虽然不通过标准 UI,但可以通过异步 SOQL (Asynchronous SOQL) 和自定义的 Lightning Web Components (LWC) 来查询和展示归档数据,实现某种程度的“在线归档”。

缺点:

  • 功能限制:Big Objects 不支持触发器 (Triggers)、流程 (Flows) 或标准报表。查询能力也受限于异步 SOQL,灵活性不如标准 SOQL。
  • UI 缺失:需要额外开发自定义组件来为用户提供查看归档数据的界面。
  • 关系维护复杂:虽然可以存储原记录的 ID,但无法像标准对象那样建立真正的关系查找。

策略二:平台外归档 (Off-Platform Archiving)

这是最灵活、也是最强大的归档策略。其核心思想是将数据从 Salesforce 抽取 (Extract)、转换 (Transform) 并加载 (Load) 到一个外部存储系统中。这个外部系统可以是云数据仓库(如 Snowflake, Google BigQuery, Amazon Redshift)、数据湖(如 AWS S3),或传统的本地数据库。

工作原理: 利用 ETL (Extract, Transform, Load) 工具(如 MuleSoft, Informatica, Jitterbit)或自定义的 API 集成,定期执行归档作业。作业会通过 Bulk API 2.0 从 Salesforce 高效地抽取大量数据,进行必要的格式转换(例如,将关系 ID 保存下来),然后加载到目标外部系统中。为了让用户能够访问这些数据,通常会构建一个集成层。例如,在 Salesforce 记录页面上放置一个 LWC,当用户需要查看历史数据时,该组件会实时调用一个外部 API,从归档数据库中拉取并展示数据。

优点:

  • 极高的可扩展性:外部数据仓库几乎可以无限扩展,轻松处理数百亿级别的记录。
  • 强大的分析能力:数据进入外部数据仓库后,可以利用强大的 BI 工具(如 Tableau, Power BI)进行复杂的分析和报告,这是 Big Objects 无法比拟的。
  • 成本优化:云存储和计算的单位成本远低于 Salesforce 平台。
  • 灵活性:可以完全自定义数据模型、索引策略和访问控制。

缺点:

  • 复杂性高:需要设计、构建和维护一个完整的外部系统,包括 ETL 流程、数据库、API 服务和前端组件。这需要专业的集成、数据库和 DevOps 技能。
  • 安全与合规:数据离开 Salesforce 平台,意味着需要为外部系统建立一套同样严格的安全和合规控制措施。
  • 数据恢复挑战:将数据从外部系统恢复 (Restore) 回 Salesforce 是一个复杂的过程,需要仔细设计以确保数据完整性。

策略三:第三方 AppExchange 解决方案

对于希望快速实施而无需投入大量自研资源的企业,AppExchange 市场提供了成熟的第三方归档解决方案(如 OwnBackup Archiver, Odaseva, Grax 等)。

工作原理: 这些通常是托管的解决方案,提供了一个配置界面,让管理员可以定义归档策略(例如,“归档所有关闭超过 3 年的 Case”)。这些应用在后台处理所有复杂的 ETL、数据存储和访问控制。它们通常还会提供预置的组件,让用户可以在 Salesforce 界面内无缝地查看和搜索归档数据。

优点:

  • 快速实施:开箱即用,大大缩短了项目周期。
  • 功能全面:通常集成了归档、备份、恢复和合规管理等多种功能。
  • 维护成本低:供应商负责底层基础设施的维护和升级。
  • 用户体验好:通常提供无缝的 UI 集成,对最终用户透明。

缺点:

  • 许可费用:需要持续支付软件许可费用,可能高于自建方案的长期基础设施成本。
  • 灵活性有限:虽然功能强大,但可能无法满足一些高度定制化的特殊需求。
  • 供应商锁定风险:数据存储在供应商的系统中,迁移到其他方案可能会很困难。

示例代码

以下示例展示了如何使用 Batch Apex 将旧的 `Case` 记录归档到名为 `Archived_Case__b` 的 Big Object 中。这是一个典型的原生平台归档实现。此代码结合了 Salesforce 官方文档中关于 Batch Apex 和 Big Objects DML 操作的最佳实践。

/**
 * @description Batch Apex class to archive old Case records into a Big Object.
 * This class queries for cases closed more than 1095 days ago,
 * inserts them into the Archived_Case__b Big Object, and then deletes the original records.
 */
global class ArchiveOldCasesBatch implements Database.Batchable<sObject> {

    /**
     * @description The start method is called at the beginning of a batch Apex job.
     * This method collects the records or objects to be passed to the execute method.
     * Based on Salesforce official documentation for Batch Apex.
     * @param bc A Database.BatchableContext object.
     * @return A Database.QueryLocator object that contains the records to be processed.
     */
    global Database.QueryLocator start(Database.BatchableContext bc) {
        // 查询截止到 3 年前(1095天)已关闭的 Case 记录
        // Archiving criteria: Cases that are closed and have a ClosedDate older than 3 years.
        Date archiveDate = Date.today().addDays(-1095);
        String query = 'SELECT Id, CaseNumber, Subject, Status, Priority, CreatedDate, ClosedDate, AccountId FROM Case WHERE IsClosed = true AND ClosedDate < :archiveDate';
        return Database.getQueryLocator(query);
    }

    /**
     * @description The execute method is called for each batch of records passed to the method.
     * This method processes the data and prepares it for insertion into the Big Object.
     * @param bc A Database.BatchableContext object.
     * @param scope A list of sObjects to be processed.
     */
    global void execute(Database.BatchableContext bc, List<Case> scope) {
        List<Archived_Case__b> casesToArchive = new List<Archived_Case__b>();
        
        // 遍历批处理范围内的 Case 记录
        for (Case c : scope) {
            // 将 Case 字段映射到 Big Object 的自定义字段
            // The __b suffix denotes a Big Object.
            Archived_Case__b archive = new Archived_Case__b(
                Original_Case_Id__c = c.Id, // 保存原始 ID 用于追溯
                Case_Number__c = c.CaseNumber,
                Subject__c = c.Subject,
                Status__c = c.Status,
                Priority__c = c.Priority,
                Created_Date__c = c.CreatedDate,
                Closed_Date__c = c.ClosedDate,
                Account_Id__c = c.AccountId
            );
            casesToArchive.add(archive);
        }

        if (!casesToArchive.isEmpty()) {
            // 使用 Database.insertImmediate 将记录插入 Big Object
            // This method is recommended for Big Object DML operations as per Salesforce documentation.
            List<Database.SaveResult> saveResults = Database.insertImmediate(casesToArchive);

            List<Id> successfullyArchivedIds = new List<Id>();
            
            // 检查插入结果,只删除成功归档的原始记录
            for (Integer i = 0; i < saveResults.size(); i++) {
                Database.SaveResult sr = saveResults[i];
                if (sr.isSuccess()) {
                    // 从 Big Object 记录中获取原始 Case Id
                    successfullyArchivedIds.add((Id)casesToArchive[i].get('Original_Case_Id__c'));
                } else {
                    // 强大的错误处理至关重要
                    // Log the error for the failed Big Object insertion
                    for(Database.Error err : sr.getErrors()) {
                        System.debug('Failed to archive Case. Original ID: ' + casesToArchive[i].get('Original_Case_Id__c') + '. Error: ' + err.getMessage());
                    }
                }
            }

            // 如果有成功归档的记录,则删除原始 Case 记录
            if (!successfullyArchivedIds.isEmpty()) {
                // 为了安全起见,查询一次以确保我们删除的是正确的记录
                List<Case> casesToDelete = [SELECT Id FROM Case WHERE Id IN :successfullyArchivedIds];
                Database.delete(casesToDelete, false); // aallowPartialSuccess is false to ensure transaction control
            }
        }
    }

    /**
     * @description The finish method is called after all batches are processed.
     * Use this method to send confirmation emails or execute post-processing operations.
     * @param bc A Database.BatchableContext object.
     */
    global 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('Case Archiving Batch Job Status: ' + job.Status);
        mail.setPlainTextBody(
            'The batch Apex job to archive old cases has finished.\n\n' +
            'Job ID: ' + job.Id + '\n' +
            'Status: ' + job.Status + '\n' +
            'Total Batches: ' + job.TotalJobItems + '\n' +
            'Batches Processed: ' + job.JobItemsProcessed + '\n' +
            'Failures: ' + job.NumberOfErrors
        );
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
}

注意事项

在实施任何归档策略时,架构师必须仔细考虑以下几点:

  • 数据完整性 (Data Integrity):在归档过程中,必须确保父子关系得以保留。例如,在归档 `Case` 时,需要在归档表中记录其关联的 `AccountId`。如果父记录(如 `Account`)本身也可能被归档,则需要一个更复杂的策略来维护整个关系链。
  • 事务与错误处理:归档操作(插入到归档库和从生产库删除)必须是事务性的。如果插入归档库成功但删除失败,会导致数据不一致。代码示例中通过检查 `SaveResult` 并只删除成功归档的记录来部分解决这个问题。一个更健壮的模式是“两阶段提交”或使用一个标记字段来表示“待删除”,再由另一个独立的清理作业来完成删除。
  • API 限制 (API Limits):对于平台外归档,大量数据的抽取应使用 Bulk API 2.0 以避免超出每日 API 调用限制。对于原生归档,Batch Apex 有其自身的 Governor Limits,例如堆大小和 DML 语句数量,需要合理设置批处理大小 (batch size)。
  • 恢复策略 (Restore Strategy):归档不是单向的。必须预先设计好数据恢复流程。如果用户需要将一条归档记录恢复到生产环境中,流程是怎样的?如何处理 ID 冲突和关系重建?这通常是平台外归档策略中最具挑战性的部分。
  • 权限和可见性:谁有权访问归档数据?如果数据在平台外,需要建立独立的认证和授权机制。如果在平台内(如 Big Objects),需要通过自定义 LWC 和 Apex 来控制其可见性。

总结与最佳实践

数据归档是 Salesforce 平台长期健康运营的关键治理活动。作为架构师,我们的职责是基于业务需求、技术限制和成本预算,设计出最合适的策略。

决策框架总结:

  • 如果数据量中等、访问频率低、且希望将数据保留在平台内以简化安全管理,Big Objects 是一个值得考虑的起点。
  • 如果数据量巨大、需要复杂的分析能力、且企业拥有强大的 IT 和集成团队,那么平台外归档策略提供了无与伦比的灵活性和扩展性。
  • 如果追求快速实现、希望获得包括备份和恢复在内的全面解决方案、且预算允许第三方 AppExchange 应用通常是最高效的选择。

最佳实践:

  1. 先定义策略,再选择工具:在评估任何技术方案之前,务必与业务和法务团队合作,明确数据保留策略。确定哪些数据需要归档,归档的触发条件是什么,以及数据需要保留多长时间。
  2. 为访问而设计:不要把归档看作是“删除”。它本质上是“迁移”。从第一天起就要设计好用户如何访问归档数据,确保归档过程对业务的干扰最小化。
  3. 自动化、监控与告警:归档应该是自动化的后台流程,而不是手工作业。同时,必须建立完善的监控和告警机制,以便在作业失败或出现异常时及时通知管理员。
  4. 彻底测试:在 Full Sandbox 中使用接近生产环境的数据量进行端到端的测试,包括归档、数据校验、访问和恢复的全过程。确保性能、数据完整性和错误处理都符合预期。

通过深思熟虑的规划和实施,一个有效的归档策略将成为您 Salesforce 架构中的基石,确保平台在未来多年里依然能够敏捷、高效地支持业务发展。

评论

此博客中的热门博文

Salesforce 登录取证:深入解析用户访问监控与安全

Salesforce Experience Cloud 技术深度解析:构建社区站点 (Community Sites)

Salesforce Data Loader 全方位指南:数据迁移与管理的最佳实践