ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

跨系统操作做了一半怎么办?Saga 补偿与人工对账实战

跨系统操作做了一半怎么办?Saga 补偿与人工对账实战 跨系统操作做了一半怎么办Saga 补偿与人工对账实战《企业级 Workflow 实战从审批流到 AI Agent》· 第 10 篇 / 共 24 篇贯穿项目星河设备 AcmeFlow 客户开通中心。本篇交付ERP 建档与权益开通的 Saga 决策、明确失败与未知结果的分流、补偿失败的人工出口以及三个独立 SQLite 文件组成的可复现机制实验。范围说明三个 SQLite 文件分别模拟协调器、ERP 和权益服务的独立提交它们不是实际 HTTP 服务也未验证网络、认证、消息投递或崩溃恢复的完整生产语义。销售收到客户电话“合同签了钱也到账了什么时候能用维保服务”运营在 AcmeFlow 里检查审核财务核对付款系统把申请标成READY。后台随即向 ERP 创建服务记录ERP 返回成功接下来调用权益平台权益平台却说该套餐配置暂时不能开通。业务同事打开后台只看到“申请失败”四个字。他们不知道 ERP 是否已经有一条有效服务记录也不知道应该再试权益、取消 ERP还是等管理员来处理。更麻烦的情况是ERP 或权益平台实际上已完成操作但响应在网络中丢失。AcmeFlow 只知道“没有收到成功”不能因此推出“远端没有执行”。如果系统把未知结果当失败重新创建 ERP 记录可能产生重复如果直接执行取消也可能把已经开通成功的服务撤掉。跨系统流程的关键不是把每一步都写成一个try/except而是在部分成功以后仍能解释远端事实并明确下一步责任。这就是本篇讨论 Saga 的出发点。我们将一个业务开通拆成两个远端动作创建 ERP 服务记录、开通服务权益。每个系统各自提交本地结果协调器记录进度若后续动作明确失败就按业务政策尝试反向动作若结果未知先查询与对账若反向动作也失败进入有责任人的人工处置而不是伪造一个“全部回滚”的成功状态。图 1READY代表审批、签署与到账等前置条件满足只有 ERP 与权益平台的效果都得到确认申请才能进入ACTIVE。一、先确认业务前置条件再谈 Saga第 09 篇完成多人审核以后申请可以得到一个有效的APPROVED结论。但APPROVED还不是开通条件合同可能尚未签署款项可能尚未核对。第 01 篇的模型已经把签署事实和到账事实分开。今天我们假设这些事实已经由可信渠道核对approved_version material_versionsigned_version material_versionpaid true。只有这样申请才进入READY允许向 ERP 和权益服务发起业务动作。这些条件需要在调用远端前再次检查而不能相信页面上曾显示过绿色勾选。资料可能在等待期间改版合同签署可能属于旧合同财务可能撤销了误关联付款。业务服务应以当前申请与事实记录为依据并在开始开通动作时冻结所需版本或拒绝并发改动。本篇代码使用一个简单的前置条件检查演示这个原则没有实现复杂的多版本合同和付款账务。教学边界明确便于读者关注跨系统执行的核心问题。我们把服务业务键设为tenant_id:application_id:service:v1。这串键不是随机重试 ID而是一次业务开通动作的稳定身份。ERP 创建服务记录、权益开通和后续查询都使用相同的关联键使协调器能在响应丢失后询问远端“这件业务已经发生了吗”。若每次重试都生成新键查询和幂等都会失去作用。真实系统可能还需要套餐版本、服务主体或租户隔离字段本例只保留足够解释机制的元素。为什么业务键不是申请 ID 一个字段同一申请将来可能续约、升级或更换套餐可能有多个不同服务动作一个动作也可能需要多次网络尝试。application_id识别申请业务键识别本次远端效果尝试 ID 则可识别每次调用。这三个层次在简单 Demo 里容易混用到了人工对账时就会造成“知道客户是谁却不知道查哪笔记录”的问题。二、三个系统各自提交不能靠一个本地事务解决在本篇实验里协调器保存co.sqlite模拟 ERP 保存erp.sqlite模拟权益平台保存rights.sqlite。它们是三个独立文件、三个独立连接。执行一次 ERP 创建以后即使协调器后续代码抛错也不能通过回滚自己的 SQLite 事务让 ERP 记录自动消失。换成真正的 HTTP API 以后这个事实更明显应用数据库的ROLLBACK管不到对方系统已经提交的业务效果。图 2教学程序把三个系统的事实分别写入不同 SQLite 文件。分开存储用于展示提交边界不等于模拟了真实网络及权限体系。AWS 的 Saga 模式说明把跨服务业务过程组织为一系列本地事务在失败时可以选择继续恢复或执行补偿。它同时区分编排式与协作式组织方式。我们在 AcmeFlow 使用一个明确的协调者决定下一步因为客户开通本身需要清楚的状态所有权这只是本项目的教学选择不能把 Saga 误解为“所有系统要共用一个数据库”。参考AWS Saga patterns本例的正向路径包含四个可观察阶段。第一检查READY的审批、签署和到账条件。第二把本地申请置为PROVISIONING写入开始日志。第三使用稳定业务键创建 ERP 服务记录并确认结果。第四使用同一个关联键开通权益确认后把申请置为ACTIVE。通知客户应在开通效果确认以后进行通知失败应单独重试通知不能把整条开通流程从头运行一次。注意PROVISIONING不是一个对客户承诺服务已生效的状态。它只表示协调器正在处理外部动作。ACTIVE是通过远端事实得到确认之后的结论。在此之前界面可以显示“开通处理中”与下一步责任但不能提前显示“已开通”。业务上若要求客户在 ERP 建档以后立即获得临时权益就需要增加明确的临时权限政策而不是偷用最终状态。三、后一步明确失败继续、补偿还是人工处理权益平台返回明确的业务拒绝例如当前套餐没有可开通的服务配置。这不同于短暂网络故障继续原样重试可能永远不会成功。协调器要选择下一步。若业务允许稍后修正配置并继续可把流程停在人工处理或等待修复若本轮申请应结束就需要取消 ERP 中已经创建的有效服务记录。本篇示例采用后者把ERP_CANCEL作为补偿动作。取消完成后ERP 记录仍可查询只是active0申请成为COMPENSATED。图 3补偿是独立的业务动作失败时必须继续暴露未完成状态不能把“已尝试取消”写成“已取消”。补偿不是时间倒流。ERP 记录曾被创建可能已经进入报表、触发了其他订阅或被人工查看。执行取消只是在 ERP 里形成新的事实“这条服务记录不再有效”。原来的创建与取消都应在日志中可追踪。如果服务已经交付、发票已开或权益被客户使用撤销可能涉及退款和结算不能假装一个 APIDELETE就消除了全部业务后果。本篇选择的补偿对象是尚未正式交付的有效 ERP 服务记录超过这个边界应转为人工处理或新的业务流程。表格把本例的策略说清楚远端结果本地能确认什么本例下一步不能做什么ERP 已确认创建权益已确认开通两项效果均存在ACTIVE随后发送通知不因通知失败从头创建服务ERP 已确认创建权益明确业务拒绝ERP 有效、权益未开通尝试取消 ERP成功后COMPENSATED不把已创建的 ERP 事实当作没发生权益拒绝后 ERP 取消也失败ERP 仍可能有效MANUAL_REVIEW记录取消失败原因不报告“全部回滚成功”ERP 或权益调用响应丢失远端结果未知按业务键查询查询不可用则停下对账不依据客户端超时直接重试或补偿权益查询确认已开通权益确实有效检查 ERP再确认ACTIVE不继续取消 ERP造成悬空权益是否选择补偿要看业务是否允许撤销这个动作以及撤销本身是否有可靠的确认方式。有些动作很难补偿例如已发送给客户的正式邮件、已向外部机构提交的不可撤销指令或者已实际提供的服务。针对这类动作更好的安排可能是把不可逆操作放在链条末端先完成可以验证和可以撤销的准备步骤。即使最终仍需做不可逆操作也要为失败后的业务修正留出路径。四、响应丢失时错误码不是远端事实假设 AcmeFlow 请求 ERP 创建记录ERP 成功提交但返回包在网络中丢失。客户端只看到超时如果它马上再调用一次create远端可能新增第二条记录。我们在模拟服务里采用稳定业务键创建是按键 UPSERT并在lost_response模式中“先提交远端再抛出响应丢失异常”。协调器捕获以后写入UNKNOWN再按业务键查询 ERP。如果查询到有效记录就继续开通权益如果查询不可用进入MANUAL_REVIEW等待进一步对账。图 4UNKNOWN 是客户端知识不足的状态不等于远端失败。查询 FOUND、确认不存在和仍然无法确认是三种不同结果。权益平台同样如此它可能已经开通权益响应却没有回来。此时绝不能因为“权益调用出错”就直接取消 ERP否则客户会拥有权益却失去对应的服务记录。本篇 10-E 故意模拟权益执行成功但响应丢失紧接着查询服务也不可用协调器停在MANUAL_REVIEW。等查询恢复授权处理人同时确认 ERP 与权益都有效才将本地状态修正为ACTIVE。在查询到权益有效之前系统不能把状态写成成功查询到权益有效以后也不能再把 ERP 当作需要补偿的失败步骤。另一种场景是查询明确不存在。此时仍要看远端的查询契约查询结果是否强一致业务键是否覆盖所有创建路径若远端返回“暂未找到”是否只是索引更新延迟没有这些保证ABSENT不能被随意解释成“绝对没有发生”。本篇模拟器只是立即查询同一 SQLite 数据库因此结果是确定的它没有验证真实 ERP 的索引延迟和查询权限。正式项目要在接口契约里明确“按键查询结果”的含义并决定多久后可以安全重试。第 07 篇讨论重试分类和预算这里再强调一层能重试的网络错误也不代表可以不查询就重复业务动作。对幂等的动作可以按相同业务键在预算内重试对结果未知且远端无法保证幂等的动作宁可停下来人工确认。AWS 的重试模式文档也把幂等性列为重试策略的关键考虑并指出持续重试会增加系统压力。参考AWS Retry with backoff五、补偿再次失败要有一个真正可用的出口在权益明确拒绝后协调器调用 ERP 取消。ERP 又暂时不可用或者拒绝取消当前记录。现在系统既不能报ACTIVE因为权益没有开通也不能报COMPENSATED因为 ERP 仍有有效记录。把异常吞掉并将申请设为“失败”会让运营无法判断是否留下孤儿服务记录。因此本例进入MANUAL_REVIEW保留动作日志、业务键、远端查询入口和后续授权处置动作。图 5人工处理不是“让运维直接改 status”。处理人要先核对 ERP 与权益再执行被授权的继续或补偿并记录理由。人工对账的工作单应回答具体问题申请和租户是谁审核、签署、到账的版本是否匹配ERP 服务记录的业务键是什么当前是否有效权益是否有效查询结果的时间和来源是什么先前正向与补偿动作执行到哪一步有哪些动作允许重试谁有权限执行。仅展示一个红色异常码会迫使运营去群里找开发者手动查库系统并没有真正形成可交付的故障处置能力。本篇代码提供两个简化的人工结束入口。finish_manual_compensation()先查询权益只有确认未开通时才允许取消 ERP成功后标记COMPENSATED。finish_manual_success()同时确认 ERP 与权益有效才把申请标记为ACTIVE。二者都要求样例参数actorops-authorized这个字符串只用于教学模拟授权并不是身份认证机制。真正的应用必须从可信登录会话或服务身份获取操作者再做角色与对象权限检查不能让客户端在请求体里自报这个字符串。人工处理还应该有双人复核或更细粒度权限吗取决于动作风险。补偿一个尚未启用的测试服务与撤销已经实际提供的客户权益风险不同。课程用单个授权处理人展示“异常必须有人接手”不把它描述成符合所有企业的审批安全政策。无论采用几人复核系统都要保存是谁查询了哪个远端结果、凭什么决定继续或取消、动作结果是否得到确认。六、代码实验三个文件模拟三个提交边界本篇代码在 code/demo.py。运行时创建临时目录里的co.sqlite、erp.sqlite、rights.sqlite分别使用独立 SQLite 连接。每条远端动作通过业务键写自己的数据库协调器在journal中记录尝试与结果。临时目录只用于使读者无需配置服务便可复现脚本运行完自动清理。执行命令如下python code/demo.py关键的“响应丢失”注入是先写远端再制造客户端异常defcreate_service(erp,key,modeok):erp.execute(INSERT INTO services VALUES (?,1) ON CONFLICT(business_key) DO UPDATE SET active1,(key,),)ifmodelost_response:raiseLostResponse(ERP 已提交响应丢失)这个实验能让读者亲眼看到本地异常与远端成功可以同时成立。需要严谨说明的是create_service的 UPSERT 只在本篇受控流程入口中充当可重复提交的模拟动作。若外部调用方在一次成功补偿后再次直接调用它当前模拟器可能把已取消记录重新激活它没有完整实现服务记录的生命周期约束。生产远端接口应在自身业务层区分“首次创建的重试”与“取消后的非法重建”并通过业务键、状态和权限拒绝不合规重放。同样cancel_service把active改为 0 的效果只能在确认权益未开通的前提下作为本例补偿不能把一个 SQL 更新当作所有业务的可逆保证。代码最重要的分支是先区分BusinessRejected、LostResponse和ServiceUnavailable。BusinessRejected在本例指权益明确拒绝可以进入补偿选择LostResponse表示客户端不知道结果要查询ServiceUnavailable出现在补偿或查询阶段需要进入人工处置。读者可以把这三类异常映射到第 07 篇的错误分类但不要把所有 HTTP 5xx 一律定为“远端未执行”。真实接口的错误契约和重试策略应在系统集成时单独验证。本示例还有明显的持久执行边界PROVISIONING状态、动作日志和远端调用不是一个原子事务。进程可能在远端已提交、协调器日志尚未更新时崩溃。脚本没有安排重启后自动扫描和安全重试也没有把“待发动作”通过 Outbox 与状态写入同一事务它只验证给定结果后应该作什么决定。前面第 06 篇的 Outbox/Inbox、后面第 11 篇的持久执行会处理如何在进程中断后重新发现待做工作第 13 篇再定义真正 ERP/CRM 的查询与幂等契约。把这几层边界讲清读者才不会把一个跑通的模拟器误认为已具备生产恢复能力。七、五组故障实验看远端事实再读本地状态运行输出保存在 code/expected-output.txt对应下列具名场景。每组都用断言同时检查协调器状态和远端数据库事实不能只看终端打印的文字。10-A 正常开通ERP 与权益都有效状态从READY经处理进入ACTIVE。10-B ERP 成功、权益明确拒绝ERP 被显式取消记录仍在但不再有效状态为COMPENSATED。10-C 补偿第一次失败系统进入MANUAL_REVIEW授权处理人重新查询后补偿成功。图 6每组结果对应demo.py的断言。图中的 ACTIVE、COMPENSATED 与 MANUAL_REVIEW 是本教学策略下的可观察业务结论。10-D 分别注入 ERP 响应丢失与权益响应丢失。两个远端数据库都已提交协调器通过固定业务键查询到效果后继续不产生重复的有效记录。10-E 注入权益成功、响应丢失、查询临时不可用系统暂停在人工对账随后授权处理人重新查询到两个有效记录标记ACTIVE。测试还验证了在权益已经有效的情况下人工补偿入口会拒绝取消 ERP。自行扩展时可以加入第六组让审批版本与材料版本不匹配预期开通在接触 ERP 前被拒绝。再加入第七组在权益已开通但发送通知失败时只重试通知ERP 和权益表的有效记录数不应增加。还可以把补偿动作重复执行核对 ERP 最终仍是无效状态并保留取消日志。做这些实验的目的是让“不能重复开通”“不能误补偿”从文档口号变成数据库里能观察的条件。这五组实验不证明远端服务真的支持按键查询、强一致读或条件取消。真实 ERP 可能需要不同的业务键、查询接口可能延迟可见权益平台也可能不提供取消。到第 13 篇接入模拟 HTTP 系统时必须先谈这些契约再把本篇决策替换为具体适配器。业务决定稳定通信实现可以逐步演化。八、怎样把这一篇接到第 03 篇的主线第 03 篇主库的applications有 UUIDapplication_id、租户、业务键和资料版本workflow_instances有流程状态和修订号。本篇沿用同一身份语义但代码在独立 SQLite 沙盒中运行。正式接入时申请仍由原主库拥有批准结果绑定资料版本合同签署与付款确认为带来源的业务事实前置条件满足后流程实例进入READY。再增加provisioning_actions或等价动作日志记录动作类型、业务键、尝试、结果和查询依据补偿动作同样有记录不能只写在日志字符串里。不要在第 03 篇原有SUBMITTED/APPROVED/REJECTED枚举上直接跳到ACTIVE。升级需要明确新增READY、PROVISIONING、COMPENSATED、MANUAL_REVIEW与ACTIVE等阶段如何与旧实例共存也要决定已驳回与已撤回申请是否仍可能收到外部事实。第 12 篇将讨论流程版本本篇先把状态含义讲清。尤其是READY它表示允许开始外部开通动作并不表示客户服务已经生效。在主线里应由一个编排责任方安排 ERP 与权益动作。业务 API、后台 Worker、外部回执入口都不能各自看到READY就独立执行开通否则会出现多个执行者对同一申请发起相同动作。第 06 篇可靠投递给出“待做动作不会因双写丢失”的基础第 11 篇演示持久执行第 19 篇会把完整项目迁入 Temporal。选择哪一种实现都要保留同一个事实外部动作以远端系统为权威本地只有在查询或确认后才推进自己的状态。人工出口也需接回主应用而不是一个没有说明的管理员脚本。待处理实例要能被查询、分派给有权限的人人工处理动作需记录申请 ID、操作者、原因、双方远端事实、动作前后的状态。若财务款项已经到账补偿服务记录不等于退款退款、合同解除和客户沟通可能是后续独立工作。对账后的“业务结束”必须来自完整的业务规则而不是 Saga 示例里两个布尔值都变成零。动作日志怎样帮助系统在崩溃后重新判断假设协调器写入PROVISIONING后崩溃还没调用 ERP。重启时它需要知道待执行的动作是什么。另一种崩溃发生在 ERP 已经创建记录、协调器尚未来得及写ERP_CREATECONFIRMED之间。此时本地日志看似没有成功远端却已有事实。两种情况不能采用同一个“重跑所有步骤”的方法。稳妥的恢复流程会先读取本地动作意图与业务键再通过远端查询确认是否已经发生只有在远端契约允许并且当前事实支持时才重试正向动作。这也是为什么动作日志不能只写step2。它至少要保存申请与租户、动作类型、稳定业务键、预期对象版本、最近一次尝试、已知结果、下一次检查时间和人工处理标记。若某次调用返回了远端记录 ID应保留关联若只是超时则结果应写 UNKNOWN不把网络异常翻译成业务拒绝。日志中的“已安排动作”和“已观察到结果”是两种不同信息后续第 11 篇的持久执行仍要维护这个区别。主应用数据库与待发动作之间也有双写问题申请变为 READY但“请创建 ERP”的消息还没发出时进程崩溃会留下没人执行的申请。第 06 篇的事务发件箱可以把业务状态与待投递意图写在同一数据库事务再由后台发送消费者仍须面对重复投递、未知远端结果和幂等键。Outbox 帮助找回“应当发出的动作”不会自动保证 ERP 只产生一次业务效果。把它与本篇的查询对账结合才接近完整恢复路径。参考AWS Transactional outbox最后客户沟通也要和业务效果保持顺序。若 ERP 已建档而权益仍未知给客户发“开通成功”会制造错误承诺若权益实际已开通但通知失败重复 ERP 创建又会制造新的数据问题。通知应由“已确认 ACTIVE”的事实驱动并有自己的发送键与重试记录。客户看到的是一个承诺运维看到的是多系统事实两者若不一致系统需要能说明差距和下一步而不是让客服猜后台到底发生了什么。补偿动作自身也需要幂等和生命周期保护很多实现只给正向创建请求安排幂等键却把“取消”当作一次性清理。补偿请求同样可能超时ERP 已把记录设为无效但响应丢了协调器重试取消时远端应该返回“已经处于目标状态”或提供可查询的结果而不是再次触发退款、释放资源或向客户重复发送取消通知。若取消会产生额外财务效果必须为它定义独立的动作键、效果记录和重试契约。原始创建键与补偿动作键可以关联含义却不同不能让一个含糊的请求 ID 同时代表两种相反操作。正向和补偿之间还存在竞争。假设权益平台响应很慢协调器误以为失败并发起 ERP 取消权益随后完成开通远端就形成“ERP 无效、权益有效”的不一致。正确做法是把 UNKNOWN 当作未知先查询或等待明确回执在决定补偿以前再核对权益是否已有有效记录。若远端支持条件取消或状态版本应在适配器里利用它。若不支持系统至少要停止自动补偿并交给人工不能用一个本地if not rights_active()就宣称跨网络的结果永远不会在下一毫秒变化。本篇代码中的查询与取消按顺序在同一进程调用但模拟的两个远端数据库没有跨库锁也没有防止外部系统并发改变权益的协议。因此它验证的是如何根据已知事实选择动作不是“检查后补偿一定不会竞态”的生产保证。正式接口契约应讨论取消时的条件、远端版本、回执订阅和争议处理窗口。对不可逆或高价值动作必要时先让两个远端进入可保留的“预备”状态待条件确认后再提交生效若系统不支持预备状态就把风险暴露在人工处置政策里。补偿后的客户沟通也要具体。ERP 记录被取消不等于客户签署的合同自动解除款项到账也不会自动变为退款完成。运营可能需要通知客户开通失败并说明下一步财务可能需要发起退款流程法务可能需要处理合同终止。这些属于新的业务动作可以由工作流继续安排但不能在技术演示里一句“已回滚”带过。一个可交付的 Saga 要让所有未闭合的业务责任都有承接者。九、验收与 Workflow Thinking本篇可以用一句话验收ERP 成功而权益失败时系统说得清留下了什么、下一步该由谁做遇到未知结果时系统先查证而不凭感觉重试。更具体地说正常路径的两个远端效果都要有证据明确拒绝走显式补偿补偿失败进入可见、可处理的人工状态响应丢失走查询已开通权益不能被错误补偿。每条判断都在本篇脚本中有断言但生产集成还需要验证远端接口契约和崩溃恢复。Workflow Thinking为什么补偿不能叫“回滚”因为原来的 ERP 创建已实际发生反向调用会形成新的业务事实还可能失败。把它称为补偿团队就会自然追问“补偿目标是什么、如何确认、补偿失败谁处理”把它称为回滚容易误以为运行时会自动抹平所有跨系统副作用。这个用词差异最终影响代码、监控和客户承诺。下一篇进一步解释为什么一个正在等待回执或人工处理的流程在 Worker 进程重启以后还能继续以及 Temporal 如何用执行历史和重放恢复流程。它也会回到本篇留下的崩溃窗口恢复流程状态是一件事避免远端效果重复是另一件事。
返回列表