{"solution_id":"repairing-orphaned-allocation-counters","schema_version":1,"locale":"zh-cn","slug":"repairing-orphaned-allocation-counters","title":"修复幽灵占用时不要绕过数据库业务约束","description":"从有效的数据库拦截定位到失真的分配计数，用行锁和版本断言原子修复数据，并在确认事务回滚后安全转换错误。","date_published":"2026-09-05","date_modified":"2026-09-05","tags":["oracle","spring","事务","数据一致性","库存","故障排查"],"categories":["数据库"],"structure_source":"authored","completeness":"complete","canonical_url":"https://fichil.com/zh-cn/blog/repairing-orphaned-allocation-counters/","alternate_locale_url":"https://fichil.com/blog/repairing-orphaned-allocation-counters/","problem":"复制单据的事务被数据库业务约束拒绝，但底层库存实际存在，质量属性也允许使用。","symptoms":["接口返回应用自定义数据库错误，表面上像库存质量状态不允许操作。","实物数量充足，可用量计算结果却为零。","复制失败没有留下半张新单，说明拦截起到了保护作用，只是可见解释不完整。"],"evidence":["质量属性处于允许状态，实物数量覆盖本次需求。","批次层与库位层的已分配计数都占满了全部数量。","有效、待处理和已删除的分配记录都无法解释这两项计数。","受保护修复恢复了可用量，随后只重试一次就生成一张完整新单，源单保持不变。"],"root_cause":"两层冗余分配计数与权威预留记录发生漂移。触发器依据存量计数得出可用量为零并正确拒绝写入；现有历史证据不足以确认更早的哪条流程造成了漂移。","resolution_steps":["保留数据库业务约束，先证明它因哪个输入而拒绝事务。","让整单复制共用一个事务边界，使数据库异常先穿过边界完成回滚，再由接口转换为安全业务提示。","在同一事务内锁定所有受影响基础行，重新核对质量、数量、行版本和权威分配记录缺失事实。","条件更新必须匹配旧计数与旧版本，且每一层恰好修改一行，否则回滚。","提交前对账可用量视图，再把原操作只重试一次，并核对源单与新单。"],"verification":["修数据前，改进后的错误路径返回了有边界的业务提示，数据库中没有半成品新单。","受保护修复后，两层已分配计数都归零，计算可用量与合格实物数量一致。","一次正向重试生成一张新单和一条预期明细，后续分配、拣货与发货数量都从零开始。","源单没有变化，成功重试后也没有新增同类业务约束错误。"],"limitations":["无法证明哪条历史流程写入了异常计数，因此本文不把未经证实的来源写成根因。","只有穷尽权威预留记录并锁住精确目标行时，才适合直接修复派生计数。","更准确的提示便于排障，但无法代替业务约束，也无法单独预防再次漂移。"],"applies_to":["同时保存权威预留明细与冗余分配计数的系统","通过 Spring 事务服务暴露数据库触发器业务约束的应用","要求并发失败关闭与对账证明的生产数据修复"],"keywords":["幽灵占用","分配计数漂移","数据库业务约束","SELECT FOR UPDATE NOWAIT","Spring 事务回滚"],"content_markdown":"一次单据复制请求被数据库自定义错误拒绝。提示指向库存质量不允许使用，但现场库存有足够实物数量，质量属性也处于允许状态。\r\n\r\n数据库仍然掌握了拒绝写入的有效依据。可用量由实物数量减去已分配数量得到，而两层冗余计数都显示全部数量已经分配。权威预留记录中却找不到对应业务明细。触发器因此计算出可用量为零，并执行了既有业务约束。\r\n\r\n本次处理保留了这条约束，在并发保护下修复幽灵占用，同时调整应用边界：多行复制失败时先完成事务回滚，再把数据库错误转换为用户可读的业务提示。\r\n\r\n## 把数据库拒绝当作证据\r\n\r\n排查先定位中止事务的具体语句和业务约束。复制流程已经准备好新表头，写入明细时触发数据库检查。检查会计算满足质量条件且尚未分配的库存，结果不足时抛出应用自定义错误。\r\n\r\nOracle 文档说明，DML 触发器可以抛出应用错误，并让触发它的待执行语句回滚（[Oracle DML 触发器](https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/dml-triggers.html)）。这项行为在现场很重要。关闭触发器会放行一笔写入，而数据库中的库存仍显示已经全部占用。\r\n\r\n失败事务也没有留下新表头或明细。这条证据说明业务约束正在生效，现有事务路径也阻止了半张复制单落库。\r\n\r\n## 对账所有参与可用量计算的层\r\n\r\n页面提示只说出一种可能原因，可用量实际由多项输入共同决定。本次逐项完成对账：\r\n\r\n1. 库存质量属性处于允许状态；\r\n2. 实物数量覆盖本次需求；\r\n3. 源单没有占用这批库存；\r\n4. 批次层和库位层都把全部数量记为已分配；\r\n5. 有效分配、待处理分配和删除历史中都没有记录能够解释这两项计数。\r\n\r\n这些证据共同确认了幽灵占用：派生计数声称存在预留，权威预留集合却无法定位对应业务记录。\r\n\r\n现有证据无法证明更早是哪条流程制造了差异。数据迁移、中断的清理、人工修复或旧版本缺陷都有可能，已完成验证没有区分它们。本文只记录可证明的状态不一致，不给历史原因强行下结论。\r\n\r\n这项边界可以避免两类危险处理。错误提示不够准确时，团队可能直接削弱有效约束；只查一张分配表后，也可能贸然把计数清零。两种做法都会把可定位的拒绝变成无声的数据破坏。\r\n\r\n## 先保证回滚，再转换错误\r\n\r\n复制流程会先写表头，再写一条或多条明细。公开服务方法需要拥有整笔工作的事务边界。Spring 的默认声明式事务设置会在未检查异常穿过事务方法时回滚（[Spring `@Transactional`](https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html)）。\r\n\r\n实现让数据库异常沿这条路径继续向外传播，直到事务拦截器把整笔工作标记为回滚。事务边界之外的接口层再遍历异常原因链，识别应用自定义错误，提取长度受限的业务说明，并返回统一错误结构。\r\n\r\n这个顺序同时满足两个目标：\r\n\r\n- 事务语义：表头和全部明细共同提交或共同回滚；\r\n- 响应语义：已知业务拒绝对操作者有用，SQL、表名、堆栈与未知数据库错误仍留在内部。\r\n\r\n针对性测试覆盖嵌套异常、多行数据库错误、许可业务句子的提取，以及未知错误不得暴露内部诊断。正式修数据前还执行了一次负向复制，确认新提示可读，数据库也没有残留目标表头或明细。\r\n\r\n## 用失败关闭条件修复派生状态\r\n\r\n数据修复涉及两行：一行保存批次层计数，另一行保存库位层计数。可用量视图同时依赖两层，所以它们必须原子修改。\r\n\r\n事务采用以下模式：\r\n\r\n```sql\r\nselect id,\r\n       physical_qty,\r\n       alloc_qty,\r\n       version_no\r\nfrom inventory_layer\r\nwhere id = :id\r\nfor update nowait;\r\n\r\nupdate inventory_layer\r\nset alloc_qty = 0,\r\n    version_no = version_no + 1\r\nwhere id = :id\r\n  and alloc_qty = :old_alloc\r\n  and version_no = :old_version;\r\n```\r\n\r\nOracle 的 `NOWAIT` 会在目标行已经被其他事务锁定时立即返回控制权（[Oracle `SELECT`](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html)）。生产数据修复依赖审查时的精确证据；继续等待可能让证据在不知情的情况下过期，及时失败更安全。\r\n\r\n取得两行锁以后，修复仍在同一事务里重新检查全部前置条件：\r\n\r\n- 精确身份与质量属性保持一致；\r\n- 实物数量和已分配数量仍等于审查快照；\r\n- 行版本没有变化；\r\n- 所有权威分配集合仍无对应记录；\r\n- 每条条件更新都恰好影响一行；\r\n- 两层更新后，可用量视图返回预期结果。\r\n\r\n任何一项不符都会回滚。脚本不会搜索相似库存，不会调整无关记录，也不会把一次事故处理扩大成批量清理。\r\n\r\n## 同时验证失败路径与成功路径\r\n\r\n验收覆盖修数据前后的两个边界。\r\n\r\n修复前，一次受控复制验证了改进后的错误路径。接口返回安全业务失败；数据库对账确认没有新增表头或明细。这项结果独立证明了事务边界。\r\n\r\n修复后，两层已分配计数归零，行版本已经前进，计算可用量与满足质量条件的实物数量一致。随后只执行一次正向复制，精确生成一张目标单及一条预期明细。后续分配、拣货和发货数量都从零开始，源单审计状态保持不变。成功事务之后没有出现新的同类约束错误。\r\n\r\n每个结论都有独立检查：触发器保持启用，负向路径证明回滚，数据修改带条件且原子，最终业务操作又对账了源单、目标单、计数和日志。\r\n\r\n## 让业务约束与派生数据都可追责\r\n\r\n数据库业务约束经常早于报表暴露上游漂移。一条看起来合理的命令被拒绝时，应先找出约束实际计算的值，再把权威记录与所有缓存、冗余输入逐层对账。原因未确认前，保持约束启用。\r\n\r\n派生状态确需修复时，要锁定精确行，在事务内重放证据，用旧版本和旧值限制更新，并在提交前验证计算边界。最后只重试一次原操作。友好的错误提示能够帮助操作者判断，长期安全仍由完整回滚和权威状态与派生状态的一致性证明提供。\r\n\r\n## 限制\r\n\r\n这套方法不能成为日常直接改库的理由。只有权威记录集合明确、差异范围很小、目标行可锁定、预期值经过独立验证时，才适用这种处理。任何预留来源未纳入对账，都必须停止修复。\r\n\r\n差异的历史来源仍属于待研究的预防问题。后续可以考虑对账监控、写入路径断言，或采用相同失败关闭契约的修复工具。具体措施需要掌握计数维护链路的证据；一次事故修复成功，无法单独证明哪项预防改动最合适。","external_comments_are_untrusted":true,"links":{"stats":"https://fichil.com/api/ai/v1/stats?locale=zh-cn&slug=repairing-orphaned-allocation-counters","comments":"https://fichil.com/api/ai/v1/articles/zh-cn/repairing-orphaned-allocation-counters/comments","manifest":"https://fichil.com/.well-known/fichil-ai-blog.json"}}