旧系统重构YUEXIAN INSIGHTS

旧系统长事务拆分,先保住业务不变量,再缩短数据库占用

面向旧 ERP、订单、库存与审批系统,说明长事务盘点、提交边界、状态机、幂等重试、补偿、渐进切换和故障对账验收。

本文目录
先看结论

简明结论:长事务不能按代码行机械切开,要按可独立承诺的业务事实重新划分提交边界

本文面向维护旧 ERP、CRM、订单、库存、财务或审批系统的产品、研发、测试与运维负责人。先用生产证据找出事务真正覆盖的 SQL、锁、外部调用、人工等待和失败分支,写清余额不透支、库存不超卖、审批不越级等必须始终成立的不变量。单个数据库内需要原子成立的事实保留短本地事务;支付、物流、通知、文件处理和跨系统步骤改为有持久状态、稳定操作 ID、幂等命令与明确超时的业务流程。每一步必须区分可重试、可补偿、不可逆和需人工确认,补偿是新的业务动作,不是假装历史从未发生。灰度时旧流程与新流程只能有一个写入主责,并按业务单据、步骤版本、资源变化和最终状态逐笔对账;无法解释处理中、结果未知或补偿失败的记录时,不应扩大流量或下线旧路径。

先判断长时间发生在哪里,再决定是否拆事务

现有问题建议处理方式需要防止的结果
单库内批量写入过大按稳定业务主键分批提交,保留批次水位、幂等键和整批业务对账只缩小批次,却让一半成功的记录没有恢复或撤销路径
事务中调用外部接口先提交本地业务意图,再由可恢复任务调用外部系统并回写结果持锁等待网络,超时后既不知道外部是否成功,也无法安全重试
跨服务业务流程把流程建成有版本的状态机,逐步执行本地事务,并为失败选择继续、补偿或人工处理用一次数据库回滚的思维处理已发生的支付、通知或人工操作
只是单条 SQL 本身很慢先修查询计划、索引、扫描范围和锁竞争,不为了“微服务化”引入流程拆分根因仍在数据库,却增加消息、状态和对账复杂度
01

适用边界:事务短不等于业务一致,拆分也不等于改成微服务

本文适用于一个请求或后台任务长时间占用数据库事务,且其中混入多批数据、远程接口、文件处理或跨系统步骤的旧系统。目标是缩短数据库锁与连接占用,同时让部分完成可以被识别和恢复;它不是强制上消息队列、工作流平台或微服务。

先确认用户真正遇到的是锁等待、死锁、连接池耗尽、版本清理受阻、发布困难,还是跨系统失败无法恢复。单条更新慢、索引缺失或不合理查询计划,应先在原事务内解决;仍能由同一个数据库以短事务原子完成的核心记账,也不要为了形式拆成最终一致。只有步骤跨越独立资源、持续时间明显长于数据库事务应承受的时间,或必须等待不可控外部结果时,才进入流程级重构。

拆分后中间状态会对其他请求可见,因此必须决定哪些状态允许查询、能否修改、是否占用额度,以及用户看到什么。PostgreSQL 官方文档提醒事务不要承载超过完整性所需的工作,也不要让连接长期处于 idle in transaction;具体锁与隔离行为仍取决于实际数据库、版本、SQL 和执行计划,应在目标环境验证。

  • 明确性能症状、业务错误和迁移目标,不用“长事务”概括所有问题。
  • 列出必须保持单库原子的记账事实,不能为缩短耗时牺牲硬约束。
  • 评估拆分新增的状态存储、任务执行、监控、对账与人工处置成本。
02

还原真实事务时间线,找到锁、等待和隐藏副作用

代码中的 begin 与 commit 只是起点。ORM 自动事务、存储过程、触发器、远程数据库链接、循环更新和异常重试都可能改变实际边界,必须把一次业务操作还原为带时间与资源的步骤。

从一个稳定业务操作 ID 出发,关联入口日志、数据库会话、慢查询、锁等待、调用链和外部系统回执,画出读取、校验、写入、锁定、提交、远程调用与返回的先后顺序。记录每一步的资源、典型与高位耗时、最大数据量、持有的锁、失败类型、超时来源和重试发起方。对低频月结、批量结算、人工审批和补偿脚本单独观察,不能只用白天在线请求代表全部事务。

盘点所有可见副作用:序列号消耗、库存预占、账户流水、文件生成、短信、支付请求、合作方调用和审计记录。还要检查事务失败后触发器或外部系统是否已经收到动作。若同一入口被 Web、小程序、APP、AI 智能体或定时任务调用,为每个调用方记录操作 ID 生成、客户端重试和超时行为;否则服务端拆分后可能被上游再次提交,产生两条独立流程。

  • 事务时间线能对应到业务单据、SQL、锁、连接和外部回执。
  • 区分数据库执行、锁等待、网络等待、应用计算与人工等待。
  • 为每个副作用标记是否可查询、可重试、可撤销及最终责任系统。
03

先写不变量和承诺点,再决定每个本地事务提交什么

原长事务往往同时承担多条业务规则。拆分前应把规则分成始终成立、流程结束时成立和允许短暂偏差三类,再为每次提交定义一个可以独立解释的业务事实。

例如订单创建与订单号唯一可以在一个本地事务提交;库存预占必须通过条件更新保证可用量不小于零;支付授权由支付系统给出事实;发货只能在付款与库存状态满足时进入。不要用“第 1 步、第 2 步”代替业务语义,而应定义待处理、已预占、待支付、已支付、待补偿、补偿失败、已完成等状态及允许迁移。每次迁移带期望旧状态和版本条件,过期命令不得覆盖更新后的单据。

隔离级别不是万能开关。PostgreSQL 的 Serializable 会在无法证明等价串行执行时让某个事务以 serialization failure 回滚,应用仍需完整重试;较低隔离级别则要按实际读写路径处理并发变化。无论采用哪个数据库,都应把唯一约束、条件写入、版本号和业务规则放在同一原子边界内验证,而不是先查询、长时间计算后无条件覆盖。

  • 每条不变量都有权威数据、执行位置和并发验证方式。
  • 每个状态有进入条件、可执行动作、超时、退出条件和终止状态。
  • 状态迁移使用期望版本或条件写入,拒绝重复及过期命令破坏新状态。
04

把跨边界流程建成可恢复状态机,而不是一串不可追踪的异步调用

跨数据库或外部系统的工作无法再靠一次回滚抹去。流程需要持久化实例、步骤与尝试记录,让进程重启、消息重复和网络结果未知后仍能判断下一步。

为每次业务流程生成稳定 operationId,保存流程类型、业务主体、当前状态、版本、已完成步骤、下一执行时间、尝试次数、最近错误和人工处置标记。步骤命令携带同一 operationId 与 stepId,参与方以此实现幂等:重复收到已完成命令时返回原结果,处理中崩溃后能够确认是继续、重试还是进入结果核查。状态更新与待执行任务若落在同一数据库,可使用同一本地事务保存,避免状态已提交但后续动作永久丢失。

AWS 的 Saga 指南把跨服务流程表示为一系列本地事务,并区分继续执行与补偿恢复,同时指出参与者增加会提高调试和补偿复杂度。参与者少、关系清晰时可以事件协作;步骤多、分支多或需要集中查看人工处置时,显式编排通常更易追踪。选择应依据现有团队与平台能力,不因采用某个产品就省略业务状态、幂等和对账。

  • 流程与步骤状态写入持久存储,应用重启后可继续判定。
  • 命令、重试、回调和人工重放共享稳定 operationId 与 stepId。
  • 定时扫描只接管到期且未被其他执行者持有的任务,并记录租约或版本。
05

补偿是新的受控业务动作,不是数据库回滚的替身

退款、释放库存、撤销权益和取消物流都有各自前提、费用和失败方式。已经被用户或外部系统观察到的事实通常不能删除,只能以反向记录或后续状态进行业务修正。

逐步标记动作类型:可安全重试的前向步骤、可以补偿的步骤、到达关键承诺点后只能继续完成的步骤、以及必须人工判断的不可逆步骤。为每个可补偿动作定义触发条件、目标状态、幂等键、最大尝试、超时与升级负责人。例如库存释放只释放本流程仍持有的预占;退款引用原支付并核对金额;已发出的通知不能“撤回事实”,只能发送更正并留下两次记录。

Microsoft Azure 的 Saga 资料将补偿描述为独立事务,并提醒跨服务流程缺少传统事务的隔离,可能出现丢失更新、脏读式业务暴露或步骤间读取变化。因而其他请求读取处理中数据时,要按状态限制操作、重新读取版本或使用业务级占用标记。补偿失败不得把原流程伪装成已取消,应停留在可查询的异常状态,保护相关资源并提供人工完成或继续前进的入口。

  • 每个步骤明确可重试、可补偿、不可逆或需人工确认。
  • 补偿引用原操作并保留双向关联,不删除原流水或审计事实。
  • 补偿连续失败会升级告警和人工队列,不以无限重试掩盖问题。
06

实施步骤:从一个可对账场景开始,保持新旧写入主责唯一

先选择业务价值明确、参与方有限且能够构造故障的流程试点。不要在同一批次同时拆事务、换数据库、改全部接口和重写业务规则,否则差异无法归因。

第一步冻结旧流程行为样例和不变量测试;第二步增加 operationId、状态和结果查询,但仍由旧路径完成业务;第三步在隔离环境用脱敏样本运行新状态机,覆盖重复、乱序、超时和恢复;第四步对生产只做影子判定,计算新流程将采取的动作但不产生写入或外部副作用;第五步选择低风险租户、组织或单据类型,把入口路由到新流程,并确保旧执行器拒绝同一范围;第六步逐批扩大,持续比较业务结果、步骤耗时、锁等待和人工异常量。

切换边界记录路由规则版本、首末业务单据、仍由旧流程处理的在途事务和新流程接管的 operationId。旧流程已经开始的单据原则上由旧流程完成,除非有经过演练的状态映射;不能把数据库中“处理中”一词直接翻译成新状态。回滚是停止新入口并让已进入新流程的实例继续、补偿或人工收敛,不是把所有单据重新交给旧入口执行一遍。

  • 试点范围、写入主责、观察阈值、停止条件和回滚责任均已批准。
  • 影子阶段不写业务、不调用外部系统,只比较规则与计划动作。
  • 新旧在途实例分别收敛,任何单据不会同时由两套执行器推进。
07

验收与风险:同时证明锁占用下降、业务结果正确和异常能够收敛

拆分成功不能只看平均接口变快。要在代表性数据与并发下比较事务持续时间、锁等待、连接占用和吞吐,也要逐笔证明重复、超时、崩溃与补偿失败不会产生不可解释的业务状态。

验收矩阵覆盖提交前崩溃、提交后回执丢失、执行器重启、同一命令重复、步骤乱序、外部调用超时但实际成功、状态版本冲突、补偿失败、人工重放和切换中回滚。按 operationId 核对入口请求、每个本地提交、资源变化、外部回执、补偿记录和最终业务状态;金额、数量、库存、权益等使用业务汇总与流水双向对账。对处于处理中和结果未知的实例设最大年龄与责任人,不能从成功率分母中删除。

性能验收固定资源、数据分布和负载,比较数据库事务时长分布、锁等待时间、死锁或序列化失败、连接池等待、流程端到端时长和待执行积压年龄。短事务可能降低锁占用,却增加最终完成时间与中间状态数量;只有用户体验、业务正确性和运维复杂度均在约定边界内才算通过。上线后保留旧行为证据和回滚开关到观察期结束,再移除旧存储过程、触发器或执行入口。

  • 数据库指标与端到端业务完成指标使用同一测试窗口。
  • 所有结果未知、补偿失败和超龄实例都有可查询明细及处置人。
  • 旧执行入口仅在新流程稳定、在途清零和回滚窗口结束后退役。

需求讨论中的问题

把一个大事务改成每执行几条 SQL 就 commit,可以吗?

只有批次之间本来就能独立承诺,并且具备水位、幂等、失败恢复与整批对账时才可以。按行数机械提交会暴露半完成状态,失败后既不能整体回滚,也不知道从哪里安全继续。

使用 Saga 后,业务就能达到数据库事务一样的强一致吗?

不能这样承诺。Saga 通过本地事务、继续执行和补偿让流程收敛,但步骤间状态可能已被观察,也没有天然的跨服务隔离。关键硬约束仍要放在能够原子执行的权威边界内。

外部接口超时后直接重试,为什么还需要结果核查?

超时只表示调用方没有按时收到结果,不等于外部系统没有执行。应使用稳定幂等键查询或重试,并把结果未知保留为明确状态;盲目生成新请求可能重复扣款、发货或通知。

拆分后接口响应更快,就能下线旧流程吗?

还不能。需要确认在途实例能收敛、异常与补偿可处置、业务流水对账一致、固定负载下数据库占用确实改善,并完成切换中回滚演练后再进入退役观察期。

带走这份清单

旧系统长事务拆分上线验收清单

可逐项勾选,辅助本次阅读;刷新页面后不保留。

参考资料

技术概念可进一步阅读以下资料;文中的业务场景与实施清单用于方案讨论。

  1. PostgreSQL Documentation · Transaction Isolation

    核对各隔离级别、Serializable 失败需重试,以及事务只承载完整性所需工作的官方说明;其他数据库应按产品版本验证。

  2. AWS Prescriptive Guidance · Saga pattern

    核对跨微服务流程由一系列本地事务协调、失败时设计补偿,以及参与者增加会提高调试与实施复杂度。

  3. Microsoft Azure Architecture Center · Saga distributed transactions pattern

    核对可补偿、关键承诺点和可重试步骤,以及 Saga 缺少跨服务隔离时需要处理的数据异常;具体实现仍按项目选择。

#长事务拆分#事务边界#状态机#补偿对账
从阅读,到行动

旧事务一旦超时就没人敢重试,也无法判断做到哪一步?

提供脱敏后的业务流程、事务时间线、关键表、外部接口和失败样例,月弦科技可以协助划分提交边界、状态机、补偿与验收范围。