Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 3 章 生产系统治理、保障与技术债务:韧性架构的动态平衡

生产系统最大的错觉,是把一次次线上成功误认为系统天然可靠。事实上,只要系统仍在承载业务、不断接入新需求、持续经历变更,它就一定会沿着更复杂、更脆弱、更难解释的方向滑动。技术债会累积,保障措施会老化,组织纪律会松动,最终在某个看似偶然的流量峰值、配置变更或下游抖动中集中爆发。

这就是我理解的软件系统“熵增定律”:孤立的生产系统,总是倾向于增加混乱度。工程师的工作,本质上不是追求一个永远完美的架构,而是不断向系统注入“负熵”,在业务速度、技术债务和生产保障之间寻找动态平衡。

3.1 引言:软件系统的熵增定律与“救火队长”陷阱

很多团队在发展初期都经历过同一条路径:先用最短时间把业务跑起来,再靠人盯、人补、人扛去渡过每一次线上波动。短期看,这种方式似乎很高效;长期看,它往往把团队拖进“越忙越救火,越救火越没有时间治理”的恶性循环。

真正难的,从来不是把系统做上线,而是让它在十倍流量、百次变更、多人协作和多系统耦合之下,仍然保持可预测、可恢复、可审计和可演进。生产事故很少是纯偶发事件,更常见的情况是:技术债长期积累,在某一次变更、流量冲击或依赖抖动下突然开始收利息。

所以,本章要讨论的不是狭义上的“稳定性技巧”,而是一套更完整的生产系统方法论:治理负责治本,保障负责治标,技术债是必须正视的现实约束。三者不是并列话题,而是一个持续运转的反馈系统。

3.2 全景框架:治理、保障与技术债务的动态反馈环

如果把生产系统放到更长的时间维度里看,技术债、治理和保障之间大致构成一个闭环:

【生产治理(治本)】 ─── 指导 / 注入机制 ───► 【技术债务(利息)】
        ▲                                           │
        │ 动态演练 / 事故复盘 / 规则固化            │ 隐形挤占产能 / 诱发事故
        │                                           ▼
【应急保障(治标)】 ◄──── 承载压力 / 爆发故障 ─── 【生产运行(现实)】

这个闭环里有四个关键判断:

  1. 技术债是因,生产故障往往是果。
  2. 保障是防线,但保障不能代替治理。
  3. 治理的价值,不是“多立规矩”,而是持续把偶发经验沉淀成系统能力。
  4. 事故、演练、复盘和数据,会反过来暴露新的技术债,并推动下一轮治理。

因此,一个成熟团队不会把治理、保障和债务拆开看,而是会不断追问:今天的故障,背后暴露的是哪类债务?今天加的一道保障,究竟是临时止血,还是已经沉淀成长期机制?

3.3 技术债务篇:如何与系统的“慢性癌症”和平共处

技术债务最危险的地方,不是代码丑,不是注释少,也不是架构图不好看;真正危险的是,它会悄悄吞掉团队的变更速度、恢复能力和判断空间。很多系统不是死于一次大故障,而是死于长期“小病不断”,最后任何小改动都可能引发连锁反应。

但老系统治理的现实从来不是“零负债”,而是“让债务处于可控带宽内”。因此,技术债务管理的重点不是道德审判,而是分级、量化和准入。

从实践上看,技术债至少可以分成五类:

  • 架构债:边界混乱、依赖蔓延、共享资源池过多。
  • 代码债:复杂度失控、测试缺口、变更影响面难以评估。
  • 数据债:口径不一致、状态无法追溯、历史快照缺失。
  • 运维债:回滚困难、告警失真、恢复工具薄弱。
  • 组织债:没有 owner、没有还债预算、没有变更纪律。

面对这些债务,我更建议建立三套机制:

  1. 红绿灯准入机制。不是所有债务都不能借。业务抢跑阶段允许存在“绿灯债”,例如先靠人工值守兜底,但要明确转红阈值,例如用户量、调用量、商家数或金额规模达到什么水平后必须重构。
  2. 还债预算制。把技术债纳入季度和版本计划,固定拿出一部分产能处理高利息债务,而不是永远等“有空了再说”。
  3. 线上重构方法论。真正成熟的重构,不是停机重写,而是“空中换引擎”:先封装抽象隔离层,再做影子系统和灰度比对,最后小流量切换、可逆迁移。

带着这个视角再看可靠性,就会发现:保障体系解决的是“今天别死”,治理体系解决的是“下次别再这么死”,而技术债务决定了系统到底有多容易在关键时刻出事。

3.4 生产治理篇:面向失效设计的韧性架构

生产治理和应急保障最大的区别,在于前者讨论的是“系统为什么总会在相似场景下反复出问题”,后者讨论的是“问题已经发生时怎么把损失控制住”。如果说保障是在事故当天保护业务,治理就是在事故之间的每一天,持续削弱下一次事故发生的概率和爆炸半径。

很多团队在稳定性建设上最容易犯的错,是把治理理解成一组零散的技术动作:补一个监控、加一个限流、写一个预案、做一次演练。这样做当然有帮助,但还不够。真正的治理,必须把这些动作变成一套持续生效的约束机制,让系统在需求增长、人员流动和架构演进中,仍然保持边界、纪律和反馈回路。

从这个角度看,生产治理至少要回答五个问题:

  1. 我们究竟在为哪些业务结果负责,而不是只为哪些机器指标负责?
  2. 什么样的故障是可以接受的,什么样的故障一旦发生就必须立刻止损?
  3. 哪些依赖可以降级,哪些依赖绝不能带病放行?
  4. 哪些技术债当前可以接受,哪些已经开始透支系统未来?
  5. 事故、演练和变更产生的经验,是否真的被沉淀成了下一轮治理规则?

下面这些能力看起来像是“稳定性工程细节”,但它们真正的价值,在于把经验变成规则,把规则变成默认约束,把默认约束变成系统长期可演进的基础。

业务系统的可靠性至少包含四层含义:

层次关注问题典型例子
可用性用户能不能完成关键动作能否搜索商品、进入结算、提交订单、完成支付
正确性系统结果是否符合业务规则不能超卖、不能重复扣款、不能错误计价
可恢复性出错后能否回到一致状态支付回调失败后可对账,库存预占失败后可释放
可解释性问题能否被定位、审计和复盘一笔订单为什么卡住,哪一步失败,谁处理过

很多团队把可靠性理解成“服务不挂”。这只是最低层目标。对电商、支付、交易、供应链这类业务系统来说,更重要的是关键业务结果不出错

例如:

  • 商品详情页推荐模块失败,可以降级为空。
  • 搜索排序模型失败,可以切到默认排序。
  • 购物车展示价失败,可以提示“以结算页为准”。
  • 订单创建失败,不能生成半张订单。
  • 支付回调重复,不能重复推进订单状态。
  • 库存扣减失败,不能假装扣减成功。

可靠性工程的第一原则是:按业务重要性分层保护,而不是按技术组件平均用力。

很多业务系统的真实发展路径,并不是一开始就有完整的平台化和治理体系,而是先为了验证业务价值快速上线,再逐步补齐可靠性能力。这种路径本身没有问题,问题在于团队常常把“已经能跑”误当成“已经可生产”。

以供应商 Feed 同步系统为例,一个典型的演进节奏通常如下:

阶段目标常见范围
PoC先验证链路能不能跑通申请 Partner、下载样例 Feed、实现单城市同步和 DB 入库
MVP先支撑业务启动打通全 Pipeline,引入 Airflow 调度,补上基础监控
生产化让系统在增长中保持稳定分片、限流、告警、测试大规模数据和高频调度
优化迭代用真实数据反向优化成本和质量监控实际变更率,动态调整同步频率和资源水位

如果从时间上看,这个过程也很常见:

  • PoC(1-2 周):申请 Partner,下载样例 Feed,实现单城市同步和 DB 入库,重点是确认字段能不能解析、业务流程能不能闭环。
  • MVP(4-6 周):扩成完整 Pipeline,引入 Airflow,补上基础成功率、延迟、失败量监控,重点是让业务能开始用,而不是先把所有平台能力做满。
  • 生产化(2-4 周):补齐分片、限流、告警、容量验证和大规模测试,重点是防止数据量、城市数、供应商数一上来就把早期实现拖垮。
  • 优化迭代:上线后持续监控实际变更率,再调整抓取和同步频率,避免既没有必要地高频拉取,也不要因为频率过低让数据失去业务价值。

这里最容易被忽略的是:PoCMVP 阶段为了抢时间,通常会暂时跳过很多生产治理动作,例如分片策略、重试预算、告警分级、回放工具、压测、审计、容量模型。这在阶段性上是合理的,但前提是团队明确知道这些是技术债,而不是“以后再说的细节”。

一个成熟团队会把这种分阶段建设显式写进方案里:

  1. 第一阶段定义“验证什么能不做”。
  2. 第二阶段定义“哪些能力缺失仍可控”。
  3. 第三阶段明确“达到什么标准才算可生产”。
  4. 上线后用真实指标反向推动优化,而不是一直凭经验拍频率、拍容量、拍阈值。

所以,可靠性治理并不要求团队一开始就过度设计;它真正要求的是:在快速上线之后,能系统性地把生产化和治理工作补回来,并且让这些工作进入版本计划,而不是永远排在需求之后。

3.4.1 SLI、SLO、SLA 与错误预算:把稳定性变成治理语言

可靠性建设必须先量化目标。没有目标,监控只是图表;没有目标,告警只是噪声;没有目标,稳定性投入永远会输给短期需求。

最常用的概念是 SLI、SLO 和 SLA。

指标含义谁使用示例
SLIService Level Indicator,服务表现指标工程团队订单创建成功率、P99 延迟、支付回调处理延迟
SLOService Level Objective,内部稳定性目标工程与业务团队订单创建 99.95% 在 2s 内完成
SLAService Level Agreement,对外协议承诺客户、法务、商业团队可用性低于承诺时补偿客户

SLI 是观测,SLO 是目标,SLA 是契约。三者不要混用。很多团队只有 SLA,却没有可执行的 SLO;最后稳定性问题就只能靠事故后吵架,而不是事故前治理。

3.4.1.1 业务系统常见 SLI

SLI 不能只看接口成功率。业务系统至少要覆盖下面几类指标:

类型指标说明
成功率下单成功率、支付成功率、库存预占成功率用户是否完成关键动作
延迟PDP P99、结算 Init P99、创单 P99、支付回调处理耗时用户等待多久,链路是否退化
正确性价格差异率、超卖次数、重复扣款次数、非法状态迁移次数是否出现资损或业务错误
新鲜度商品索引同步延迟、库存同步延迟、供应商数据延迟读模型和权威数据相差多久
完整性消息堆积、DLQ 数量、补偿任务 backlog异步链路是否持续收敛
饱和度线程池、连接池、队列、CPU、内存、磁盘、网络系统距离崩溃还有多远

面向用户体验的 SLI 更适合做告警;面向资源和组件的 SLI 更适合做排障。不要把两者颠倒。CPU 高不一定是事故,但下单成功率跌了通常就是事故。

3.4.1.2 按用户旅程定义 SLO

业务系统的 SLO 最好围绕用户旅程定义,而不是围绕单个接口定义。

用户旅程推荐 SLO说明
商品浏览99.9% 商品详情页在 1s 内返回基础信息推荐、营销标签可降级
搜索导购99.5% 搜索请求在 800ms 内返回结果个性化排序失败可回退默认排序
结算初始化99.5% 结算页在 2s 内完成商品、库存、价格校验非关键权益可降级
提交订单99.95% 创单请求在 3s 内明确成功或失败不允许返回未知半成功
支付回调99.99% 合法回调在 30s 内完成幂等处理延迟处理必须进入对账
商品发布99% 商品发布后 5 分钟内同步搜索和缓存允许短暂最终一致

这里有两个关键点:

第一,SLO 要区分核心与非核心能力。推荐模块和支付回调不能使用同一套目标。第二,SLO 要明确时间窗口。比如“商品发布后 5 分钟内同步搜索”,比“搜索要及时”更可执行。

3.4.1.3 错误预算

错误预算是 SLO 的另一面。

如果订单创建成功率 SLO 是 99.95%,那么一个月内允许的失败预算是 0.05%。预算消耗过快,说明系统稳定性正在透支。此时团队应该收紧发布、暂停高风险变更、优先修复稳定性问题。

错误预算的价值不是“允许失败”,而是把稳定性和研发速度放进同一个决策框架:

  • 预算充足:可以更积极发布、实验和重构。
  • 预算快速消耗:需要减少变更,治理错误率最高的链路。
  • 预算耗尽:进入稳定性冻结期,只允许修复和低风险变更。

没有错误预算时,稳定性讨论容易变成抽象争论;有了错误预算,讨论会变成数据驱动。

3.4.2 故障模型:先知道系统会怎样坏

可靠性工程不是等故障发生后再补救,而是在设计阶段预判故障类型。

业务系统常见故障可以分为九类:

故障类型典型表现常见根因
流量冲击QPS 突增、队列堆积、接口超时大促、热点商品、爬虫、重试风暴
慢依赖P99 上升、线程池耗尽下游 DB 慢查询、RPC 抖动、外部供应商变慢
部分失败只有某地区、某品类、某渠道异常分片热点、灰度版本、地域网络、配置差异
数据不一致支付成功订单未更新、搜索仍展示下架商品异步消息丢失、补偿失败、对账缺失
资源耗尽连接池满、磁盘满、内存 OOM、Pod 重启容量低估、泄漏、突发批任务
发布回归新版本错误率升高兼容性不足、灰度缺失、回滚困难
配置事故开关误开、阈值错误、路由错误配置无审计、无灰度、无校验
外部依赖故障支付渠道、短信、供应商接口异常第三方不可控、超时和降级不足
人为操作事故误删数据、错误补偿、误切流权限过大、缺少审批和演练

做可靠性设计时,要针对每类故障问三个问题:

  1. 如何提前发现?
  2. 如何限制影响范围?
  3. 如何恢复业务和数据?

只回答“加监控”是不够的。监控只能发现问题,不能自动阻止级联故障。要让故障可控,还需要隔离、限流、熔断、降级、补偿、对账、回滚和应急流程。

3.4.3 故障域与依赖分级

大型业务系统真正危险的不是单点故障,而是故障传播。一个推荐接口变慢,拖慢商品详情;商品详情线程池被打满,网关堆积;网关重试放大流量,最终核心交易接口也被拖死。这就是故障域没有隔离。

3.4.3.1 故障域

故障域可以从多个维度划分:

维度示例隔离手段
业务域商品、交易、支付、履约服务边界、数据边界、独立发布
流量域普通流量、大促流量、爬虫流量网关限流、活动专用集群、风控拦截
用户域C 端用户、商家、运营后台租户限流、后台任务隔离
地域域华东、华南、海外多地域部署、就近路由、区域熔断
资源域线程池、连接池、队列、缓存舱壁隔离、配额、独立集群
数据域分库分表、冷热数据、租户分片分片键设计、归档、读写隔离

故障域设计的目标是:一个局部问题不应该拖垮全站,一个非核心链路不应该拖垮核心链路,一个租户或热点商品不应该耗尽公共资源。

3.4.3.2 依赖分级

依赖必须分级。否则所有失败都会被当作同等严重,系统就很难降级。

依赖等级定义失败策略
强依赖没有它不能继续业务快速失败、阻断、进入补偿或人工处理
弱依赖失败后可提供降级体验超时后返回默认值、隐藏模块、异步补齐
后置依赖不影响当前用户响应写 Outbox、异步重试、DLQ
观测依赖只影响日志、埋点、统计本地缓冲、采样、失败不阻断主链路

以结算页为例:

  • 商品上下架和库存预占是强依赖。
  • 营销标签、推荐凑单提示是弱依赖。
  • 积分发放、通知、推荐特征回流是后置依赖。
  • 埋点和日志上传是观测依赖。

只有把依赖分级写进设计,超时、重试、熔断、降级才有依据。

3.4.4 高可用架构原则

高可用不是“多部署几个实例”。它是一组贯穿应用、数据、基础设施和组织流程的设计原则。

3.4.4.1 冗余

冗余用于消除单点。

层次常见做法注意事项
应用层多实例、多 Pod、多可用区实例无状态,健康检查真实反映业务可用性
网关层多入口、多 LB、DNS 容灾避免单一入口和错误路由配置
数据层主从、半同步、多副本、备份复制延迟、故障切换、恢复演练
缓存层Redis Cluster、多副本、热点隔离主从切换期间的一致性和热点保护
消息层多 Broker、多分区、多副本ISR、磁盘、消费积压和重平衡
跨地域同城多活、异地灾备数据冲突、流量切换、成本与复杂度

冗余必须配合切换能力。没有演练过的备用链路,不能算可靠性能力。

3.4.4.2 无状态与快速恢复

应用服务尽量无状态。状态外置到数据库、缓存、对象存储或消息系统。这样实例可以快速扩缩容和替换。

但“无状态”不等于“没有状态”。业务状态仍然存在,只是不能绑死在某个进程内。常见问题包括:

  • 本地内存保存用户会话,实例重启后登录态丢失。
  • 本地队列保存待处理任务,进程崩溃后任务丢失。
  • 本地缓存没有版本控制,发布后读到旧规则。

可靠的做法是明确状态归属:什么状态可以本地缓存,什么状态必须持久化,什么状态需要幂等恢复。

3.4.4.3 舱壁隔离

舱壁隔离来自船舱设计:一个舱进水,不能让整条船沉没。

业务系统里的舱壁隔离包括:

  • 不同下游依赖使用不同线程池和连接池。
  • 核心接口和非核心接口使用不同资源池。
  • 后台批任务和在线请求隔离。
  • 大客户、活动、渠道流量隔离。
  • 支付、订单、库存等强一致链路与推荐、画像、报表隔离。

典型反模式是一个应用只有一个全局线程池。推荐服务变慢时,线程池被推荐请求占满,订单请求也无法执行。这种故障不是推荐系统故障,而是资源隔离设计失败。

3.4.4.4 优雅退化

优雅退化不是“随便返回默认值”,而是在业务允许的边界内选择更低质量但可接受的服务。

场景可接受降级不可接受降级
商品详情隐藏推荐、展示基础信息展示已下架商品为可购买
搜索导购默认排序、隐藏个性化标签返回过期价格导致误导购买
购物车部分商品状态显示“稍后刷新”静默丢失用户加购
结算非关键优惠提示缺失跳过库存校验继续下单
支付渠道切换、挂起等待对账未确认支付却标记成功

每个降级策略都应该写清楚:触发条件、用户文案、数据后果、恢复方式、审计记录和负责人。

3.4.5 保护机制:超时、重试、幂等、限流、熔断、降级

保护机制的目标是阻止小故障变成大事故。

3.4.5.1 超时预算

超时不能拍脑袋设置。一个请求的端到端超时应该拆成多个依赖的预算。

例如结算 Init 总超时 2s:

依赖建议预算说明
商品校验150ms批量查询,强依赖
库存预检查300ms强依赖,失败阻断
计价试算500ms强依赖,展示链路可降级,提交链路不可降级
营销试算300ms视业务可部分降级
地址运费300ms可缓存,可部分降级
聚合与序列化200ms应用自身预算

如果每个下游都默认 3s 超时,一个聚合接口就会在故障时被拖成几十秒。超时预算必须从入口向下传递,不能每一层各自为政。

3.4.5.2 重试

重试只适合瞬时失败,不适合所有失败。

错误类型是否重试原因
网络抖动、连接重置可以可能是瞬时问题
读请求超时可以,但要限制次数注意不要放大流量
写请求超时谨慎必须有幂等键或事务状态
参数错误、权限错误不重试重试没有意义
下游容量不足不应立即重试会加剧故障

重试要配合退避和抖动:

第一次失败:等待 50ms
第二次失败:等待 100ms + 随机抖动
第三次失败:等待 200ms + 随机抖动
超过总预算:快速失败

没有退避的重试会形成重试风暴。没有幂等的重试会形成重复副作用。

3.4.5.3 幂等

业务系统里,“只执行一次”通常做不到;“执行多次结果一致”必须做到。

场景幂等手段
创建订单idempotency_key + 唯一索引 + 请求摘要
支付回调渠道流水号唯一约束 + 支付单状态机
消息消费event_id 去重表 + 业务状态条件更新
库存释放预占单状态机 + 释放流水唯一约束
优惠券核销券实例状态机 + 核销单唯一约束

幂等设计要回答四个问题:

  1. 幂等键由谁生成?
  2. 幂等键的作用范围是什么?
  3. 相同幂等键但请求参数不同怎么办?
  4. 幂等记录保存多久,如何清理?

仅仅“前端按钮防抖”不是幂等。真正的幂等必须在服务端和存储层兜底。

3.4.5.4 限流

限流不是为了拒绝用户,而是为了保护系统在极端情况下仍然服务核心请求。

常见限流维度:

  • API 维度:保护单个接口。
  • 用户维度:防止单用户刷爆资源。
  • 商家或租户维度:防止大租户影响小租户。
  • 商品维度:保护热点商品。
  • 活动维度:隔离大促流量。
  • 下游依赖维度:保护数据库、缓存、供应商接口。

限流结果也要分层:

限流对象返回策略
非登录爬虫直接拒绝
普通用户读请求排队或提示稍后重试
下单请求尽量排队,明确失败语义
支付回调不建议简单限流,应快速落表后异步处理
后台批任务降速、暂停、转离线队列

支付回调、库存补偿、对账任务这类恢复链路不能和普通流量共用同一个限流策略,否则事故恢复时反而被限住。

3.4.5.5 熔断

熔断解决的是“下游已经异常,继续调用只会浪费资源”的问题。

熔断通常有三种状态:

状态行为
关闭正常调用下游
打开快速失败或降级,不再打到下游
半开放少量探测流量,成功后恢复,失败后继续打开

熔断触发条件不宜只看错误率,还要看延迟、并发数、超时率和业务错误类型。库存不足不是系统故障,不应该触发熔断;库存服务超时率升高才可能触发熔断。

3.4.5.6 降级

降级要提前设计,而不是事故中临时写代码。

一份合格的降级方案至少包含:

  • 哪些功能可以降级。
  • 什么条件触发降级。
  • 降级后的用户体验是什么。
  • 降级是否会造成数据不一致。
  • 如何恢复。
  • 谁有权限开启。
  • 开启和关闭是否有审计。
  • 是否演练过。

降级开关必须纳入配置治理。不要让任何人可以在没有审批、没有审计、没有回滚方案的情况下打开“允许使用缓存价创单”这类高风险开关。

3.4.6 变更治理:多数生产事故,根子都在变更

生产系统不是运行时自己突然变坏的,更多时候,是某次代码发布、配置修改、数据回填、规则切换或外部依赖接入,把原本潜伏的脆弱性显露出来。很多团队把稳定性问题归咎于“流量太大”或“依赖太差”,但回头复盘会发现,真正点燃事故的火种,往往还是变更。

所以,成熟的生产治理一定把变更当成最高风险动作之一,而不是默认“上线成功就代表变更安全”。一套可执行的变更治理,至少应该包含下面几层:

  • 风险分级:代码发布、配置变更、数据变更、权限变更不能用同一套审批标准。
  • 小步灰度:任何高风险变更都应先在小流量、小地域、小租户范围内验证。
  • 可逆设计:上线前先确认怎么回滚,而不是出事后再讨论能不能回。
  • 冻结机制:错误预算快速消耗、大促期间、关键结算窗口应主动收紧变更。
  • 证据闭环:每一次变更都能追溯到版本、负责人、影响范围和回滚动作。

从实践经验看,下面几类变更尤其危险:

变更类型常见误区推荐治理方式
代码发布以为单测过了就安全灰度、回滚预案、关键路径 watch
配置变更以为“只是改个参数”风险很低schema 校验、双人审批、灰度生效
数据变更以为脚本只跑一次不会出事限速、分批、影子校验、回填回滚
规则变更以为规则平台天然安全版本化、命中监控、误杀恢复
外部接入以为先接上再慢慢治理适配层、隔离池、降级与熔断预案

治理变更的核心不是拖慢研发,而是让团队始终知道:哪些变更在消耗系统信用,哪些变更在透支未来恢复空间。

3.4.7 混沌工程:用有计划的破坏逼出真实问题

很多技术债平时看不见,不是因为它不存在,而是因为系统还没有被真正打到边界。压测能回答“流量大了会怎样”,但不一定能回答“依赖慢了会怎样”“配置错了会怎样”“半数实例异常时系统是否还能自我保护”。这正是混沌工程的价值。

混沌工程不是为了制造戏剧化故障,更不是为了证明团队多勇敢,而是为了在可控范围内提前暴露脆弱点,逼着系统把“理论上的保护机制”变成“现实里真的有效的保护机制”。

一次像样的混沌实验,至少要说清楚四件事:

  1. 这次要验证什么假设。
  2. 影响范围控制在哪里。
  3. 触发后预期的保护行为是什么。
  4. 失败后要沉淀成什么治理动作。

例如:

  • 注入库存服务 30% 超时,验证结算链路是否正确触发限流、降级和熔断。
  • 关闭某个支付渠道回调消费,验证对账和补偿是否能在时间窗口内接管。
  • 模拟推荐服务高延迟,验证是否会挤占订单线程池。
  • 对配置中心推送错误规则到灰度环境,验证审批、审计和回滚是否顺畅。

真正有价值的混沌工程,不是最后写一句“演练完成,系统稳定”,而是逼出具体治理结果,例如:

  • 某个共享线程池必须拆分。
  • 某个补偿任务必须增加幂等键。
  • 某个降级开关必须增加审计和审批。
  • 某条告警必须改成基于业务症状而不是机器指标。

混沌工程的终点,不是“我们敢破坏系统”,而是“我们终于知道系统会怎么坏,并且愿意在真实事故前把这些问题修掉”。

3.5 应急保障篇:现代可观测性与 1-5-10 应急时钟

应急保障的价值,不是证明系统已经足够完美,而是在问题必然发生时,让团队能在最短时间看见问题、切断扩散、恢复核心业务,并为后续治理留下足够的上下文和证据链。

3.5.1 监控系统:从“有图表”到“能救命”

监控系统是可靠性工程里最容易被低估、也最容易做成表面工程的一部分。很多团队有成百上千张图,但事故来临时仍然不知道哪里坏了;有几百条告警,但真正 P0 被淹没在噪声里。

一个成熟的监控系统必须同时满足四个目标:

  1. 发现问题:用户感知异常前后,系统能尽快发现。
  2. 定位问题:能从业务指标追到服务、依赖、资源和代码路径。
  3. 解释问题:能说明影响范围、开始时间、变化点和根因候选。
  4. 推动恢复:告警附带负责人、预案、看板和止血动作。

监控不是运维团队单独负责的事情。业务系统的监控指标必须由业务研发设计,由服务 owner 负责维护。

3.5.1.1 监控分层模型

建议按五层建设监控。

层次目标典型指标
业务监控判断用户和业务是否受影响下单成功率、支付成功率、GMV、转化漏斗、资损金额
链路监控判断关键路径哪一步异常结算 Init、库存预占、计价、创单、支付回调各步耗时和成功率
应用监控判断服务自身是否健康QPS、错误率、P99、线程池、连接池、队列、依赖耗时
中间件监控判断基础组件是否退化MySQL、Redis、Kafka、Elasticsearch、对象存储
基础设施监控判断运行环境是否饱和CPU、内存、磁盘、网络、容器重启、节点压力

这五层要能上下联动。支付成功率下降时,应该能继续下钻到支付回调延迟、支付渠道错误码、数据库锁等待、消息积压和具体版本变更。

3.5.1.2 业务监控

业务监控是最接近用户体验的一层,也是告警优先级最高的一层。

电商系统建议至少建设下面这些业务指标:

业务域核心指标异常含义
商品商品发布成功率、上下架延迟、商品索引同步延迟商品无法发布或前台不可见
搜索搜索成功率、零结果率、召回数量分布、搜索转化率搜索体验异常或索引错误
购物车加购成功率、购物车读取成功率、合并失败率用户意图丢失或体验下降
结算结算 Init 成功率、库存预占失败率、价格试算失败率交易前置链路异常
订单创单成功率、订单状态推进延迟、非法状态迁移数交易主链路异常
支付支付发起成功率、渠道回调成功率、支付单挂起数资金链路异常
库存超卖次数、预占释放延迟、库存对账差异资损或履约风险
供应商同步成功率、数据新鲜度、解析失败率、DLQ 数量外部供给质量异常

业务监控要特别注意分桶。只看全站平均值会掩盖局部故障。常见分桶包括:

  • 地域:国家、城市、机房、可用区。
  • 端:iOS、Android、Web、小程序、开放 API。
  • 品类:实物、酒店、机票、券码、充值。
  • 渠道:自然流量、活动流量、推荐流量、商家后台。
  • 用户类型:新客、老客、会员、大客户、商家。
  • 支付渠道:微信、支付宝、信用卡、余额、第三方渠道。

例如全站支付成功率只下降 0.2%,可能看起来不严重;但如果某个支付渠道在某个国家下降 20%,就是明确事故。

3.5.1.3 应用监控:RED 与 USE

在线服务常用 RED 方法:

指标含义例子
Rate请求速率QPS、每分钟请求数
Errors错误5xx、业务错误码、依赖错误
Duration延迟P50、P95、P99、最大值

资源类组件常用 USE 方法:

指标含义例子
Utilization使用率CPU、内存、磁盘、连接池使用率
Saturation饱和度队列长度、线程池排队、Kafka lag
Errors错误网络错误、磁盘错误、连接失败

对业务服务来说,应用监控至少应包含:

  • 每个 API 的 QPS、成功率、错误码分布。
  • P50、P90、P95、P99、P999 延迟。
  • 入口请求大小、响应大小、批量大小分布。
  • 下游依赖耗时、成功率、超时率、熔断次数。
  • 线程池活跃数、队列长度、拒绝数。
  • 数据库连接池使用率、等待时间、获取失败数。
  • 本地缓存命中率、缓存大小、淘汰数。
  • 消息生产成功率、消费延迟、重试次数。
  • GC、goroutine/thread 数、堆内存、FD 数。

P99 比平均值重要。平均延迟稳定,不代表用户体验稳定。很多事故都是 P99 先升高,然后队列堆积,最后错误率爆炸。

3.5.1.4 中间件监控

中间件监控要关注“是否还能承载业务语义”,而不仅是“组件是否活着”。

MySQL

指标关注点
QPS / TPS流量是否异常
慢 SQL 数量访问模式是否退化
连接数 / 连接池等待应用是否被 DB 卡住
行锁等待 / 死锁事务设计是否有问题
主从延迟读写分离是否会读旧数据
Buffer Pool 命中率内存是否足够
磁盘 IOPS / fsync 延迟存储是否饱和
binlog 延迟与大小数据订阅、恢复能力是否受影响

Redis

指标关注点
QPS / 延迟是否出现热点或网络抖动
命中率缓存是否有效
内存使用 / evicted keys是否正在淘汰关键数据
hot key / big key是否存在局部热点
blocked clients是否有慢命令阻塞
replication lag主从切换是否有数据风险
连接数客户端是否泄漏连接

Kafka

指标关注点
Produce 成功率和耗时上游是否能写入
Consumer lag下游是否跟得上
分区倾斜key 设计是否导致热点
ISR 收缩副本可靠性是否下降
Broker 磁盘水位是否接近不可写
Rebalance 次数消费组是否不稳定
DLQ 数量是否存在无法自动处理的数据

Elasticsearch

指标关注点
查询延迟和错误率搜索体验是否退化
rejected / thread pool queue查询或写入是否过载
JVM heap / GC集群是否接近不稳定
segment 数量merge 压力是否过大
refresh / indexing 延迟索引新鲜度
shard 分布是否存在热点分片
circuit breaker查询是否触发内存保护

中间件指标要和业务指标联动。例如搜索零结果率升高,可能不是 ES 宕机,而是商品索引同步延迟或某个类目 mapping 错误。

3.5.1.5 指标设计规范

指标设计不规范,监控平台很快会变成垃圾场。

命名

建议统一命名:

<domain>_<component>_<metric>_<unit>

示例:

order_create_requests_total
order_create_latency_seconds
payment_callback_failures_total
inventory_reservation_conflicts_total
checkout_init_dependency_latency_seconds

标签

常用标签:

标签示例
serviceorder-service
envprod、staging
regionsg、hk、sh
clusterprimary、canary
endpointCreateOrder
methodGET、POST
resultsuccess、failure
error_codeINVENTORY_TIMEOUT
dependencyinventory-service
biz_typephysical、hotel、voucher

不要把高基数字段放进指标标签,例如:

  • user_id
  • order_id
  • sku_id
  • trace_id
  • request_id

这些字段应该进入日志或追踪,不应该进入 metrics 标签。否则监控系统会被高基数打爆。

直方图桶

延迟指标建议使用 histogram,而不是只记录平均值。桶要贴近业务 SLO。

例如创单延迟 SLO 是 3s,可以设置:

50ms, 100ms, 200ms, 500ms, 1s, 2s, 3s, 5s, 10s

如果桶全是默认值,P99 可能无法准确表达业务目标。

3.5.1.6 日志系统

日志用于回答“这一次具体发生了什么”。

成熟的业务日志应该是结构化的,而不是只打印自然语言。

{
  "timestamp": "2026-05-07T10:30:00+08:00",
  "level": "ERROR",
  "service": "order-service",
  "trace_id": "4f8c9f...",
  "span_id": "a81d...",
  "operation": "CreateOrder",
  "user_id_hash": "u_9d21",
  "order_id": "o_123456",
  "idempotency_key": "idem_abc",
  "error_code": "INVENTORY_RESERVE_TIMEOUT",
  "dependency": "inventory-service",
  "latency_ms": 850,
  "message": "reserve inventory timeout"
}

日志设计要注意:

  • 每条关键链路日志必须包含 trace_id
  • 写操作日志必须包含业务 ID 和幂等键。
  • 错误日志必须包含标准错误码,而不是只打印异常堆栈。
  • 敏感信息要脱敏,例如手机号、证件号、银行卡号。
  • 日志级别要有规范,不能把正常业务失败都打成 ERROR。
  • 采样要谨慎,关键交易失败日志不能被采样丢掉。

日志不是越多越好。无结构、无字段、无错误码的日志越多,排障越慢。

3.5.1.7 链路追踪

链路追踪用于回答“慢在哪里、卡在哪一跳”。

一次完整交易链路可能是:

Gateway
  -> Checkout Service
    -> Product Service
    -> Pricing Service
    -> Inventory Service
    -> Marketing Service
  -> Order Service
    -> MySQL
    -> Outbox
  -> Kafka
    -> Payment Service

追踪系统需要做到:

  • 网关生成或透传 trace_id
  • RPC、HTTP、MQ 都要传递上下文。
  • 每个 Span 标记服务名、方法、依赖、错误码、耗时。
  • 异步消息要把生产端和消费端通过 trace 或 event id 关联。
  • 对慢请求和错误请求提高采样率。

关键 Span 建议包含:

字段示例
biz.operationcheckout_init、create_order、payment_callback
biz.idorder_id、payment_id、checkout_id
dependencyinventory-service
error_codeINVENTORY_TIMEOUT
retry_count2
degradedtrue
idempotency_hitfalse

追踪不要只服务研发排障,也要服务事故复盘。事故时间线中每个关键业务动作都应该能找到对应 trace。

3.5.1.8 告警体系

告警不是“指标超过阈值发消息”。告警的目标是把正确的人,在正确时间,带到正确现场。

告警优先级建议如下:

等级定义示例响应
P0核心业务大面积不可用或有资损风险下单成功率大跌、重复扣款、支付状态错乱立即电话,事故群,负责人到场
P1核心链路局部异常或持续退化某地区支付失败率升高、库存预占超时10 分钟内响应
P2非核心功能异常或有恶化趋势推荐不可用、某批任务失败工作时间处理或值班处理
P3观察项或容量预警磁盘水位增长、连接池接近阈值工单跟进

告警要优先基于症状,而不是原因。

好的 P0 告警:

订单创建成功率 5 分钟内低于 98%,影响全部用户

较差的 P0 告警:

order-service CPU > 80%

CPU 高可能只是正常流量,也可能已经自动扩容;订单成功率下降才是真正用户影响。

3.5.1.9 SLO 燃烧率告警

固定阈值容易误报或漏报。更成熟的方式是错误预算燃烧率告警。

例如某接口 30 天 SLO 是 99.9%,错误预算是 0.1%。如果 5 分钟内错误率达到 5%,虽然只持续很短时间,但它燃烧预算的速度非常快,应立即告警。

常见做法是多窗口组合:

窗口作用
5 分钟快速发现突发事故
30 分钟过滤短暂抖动
2 小时发现持续退化
24 小时发现慢性问题

示例规则:

groups:
  - name: order-slo
    rules:
      - alert: OrderCreateHighBurnRate
        expr: |
          (
            sum(rate(order_create_requests_total{result="failure"}[5m]))
            /
            sum(rate(order_create_requests_total[5m]))
          ) > 0.02
        for: 5m
        labels:
          severity: P0
          service: order-service
        annotations:
          summary: "Order create failure rate is burning error budget fast"
          runbook: "https://runbook.example.com/order-create"

真实生产中还要叠加 30 分钟窗口,避免短暂毛刺直接打爆电话。

3.5.1.10 告警治理

告警治理和代码治理一样重要。没有治理,告警会腐烂。

每条告警都应该有:

  • 明确 owner。
  • 明确严重级别。
  • 明确触发条件。
  • 明确恢复条件。
  • 明确 runbook。
  • 明确看板链接。
  • 明确是否需要夜间唤醒。
  • 明确抑制和去重策略。

告警噪声常见来源:

噪声来源治理方式
阈值过低使用历史基线或分位数
同根因多告警告警聚合和抑制
非业务影响告警夜间唤醒调整严重级别
发布期间短暂波动结合变更事件和灰度窗口
无 owner 告警直接下线或补 ownership
长期无人处理每周告警复盘

一个很实用的指标是:每周每个 on-call 被夜间唤醒次数。如果次数持续过高,团队会进入告警疲劳,真正事故反而没人敏感。

3.5.1.11 看板设计

看板要按排障路径设计,而不是按组件堆图。

推荐四类看板:

业务驾驶舱

  • GMV、订单量、支付成功率。
  • 搜索、加购、结算、创单、支付漏斗。
  • 各端、各地区、各品类分桶。
  • 当前事故和变更事件。

核心链路看板

  • 结算 Init 每一步成功率和耗时。
  • 创单 Saga 每一步成功率和耗时。
  • 支付回调处理延迟。
  • 补偿任务 backlog。

服务看板

  • API QPS、错误率、P99。
  • 下游依赖耗时。
  • 线程池、连接池、队列。
  • 当前版本、实例数、重启次数。

中间件看板

  • DB 慢 SQL、锁等待、主从延迟。
  • Redis 命中率、热点 key、内存淘汰。
  • Kafka lag、DLQ、分区倾斜。
  • ES 查询延迟、rejected、heap、shard 状态。

看板第一屏应该回答三个问题:

  1. 用户是否受影响?
  2. 影响范围多大?
  3. 当前最可疑的故障域在哪里?

不要让 on-call 在事故中从几十张图里猜。

3.5.1.12 监控覆盖检查清单

上线前可以用下面的清单检查监控是否完整:

检查项必须回答
业务成功率核心动作是否有成功率指标
业务延迟用户关键等待路径是否有 P99
错误码是否区分业务失败和系统失败
依赖指标每个下游是否有成功率、耗时、超时
资源饱和线程池、连接池、队列是否可见
数据一致性是否有对账差异、补偿 backlog、DLQ
分桶维度是否能按地区、端、品类、渠道下钻
变更关联看板是否能看到版本、配置、开关变化
告警 owner每条 P0/P1 是否有人负责
Runbook告警是否附带处理步骤

监控系统做到这个程度,才真正有资格说“线上可观测”。

3.5.2 应急响应:先止血,再定位,再恢复

事故处理中最常见的错误,是一开始就追根因。真正的事故响应顺序应该是:

  1. 判断影响范围。
  2. 保护核心链路。
  3. 止血和恢复用户体验。
  4. 保留证据。
  5. 定位根因。
  6. 修复和复盘。

3.5.2.1 事故角色

P0/P1 事故建议明确角色:

角色职责
Incident Commander统一指挥,决定止血动作
Tech Lead技术定位和修复方案
Operator执行切流、回滚、开关、扩容
Communicator同步业务、客服、管理层
Scribe记录时间线和关键决策

事故中最怕多人同时操作、没人记录、没人统一决策。角色清晰比技术高超更重要。

3.5.2.2 常见止血动作

场景止血动作
新版本错误率升高立即回滚或停止灰度
单地域异常摘除地域流量或切到备用区域
下游依赖慢熔断、降级、限制并发
数据库打满限流写入、暂停批任务、扩容只读
Kafka 积压扩消费者、暂停低优先级生产、隔离热点 topic
支付渠道异常切支付渠道、挂起订单、启动渠道对账
商品错误上架下架、冻结库存、停止投放
价格错误关闭活动、冻结订单、通知客服和法务

止血动作要提前演练。事故中临时决定“能不能关这个开关”,通常已经晚了。

3.5.2.3 事故时间线

事故复盘依赖时间线。建议记录:

  • 第一次异常指标出现时间。
  • 第一次告警时间。
  • 人工响应时间。
  • 影响范围变化。
  • 每次操作时间和操作人。
  • 关键判断依据。
  • 恢复时间。
  • 根因确认时间。

没有时间线,复盘会变成各说各话。

3.5.3 容量规划、压测与故障演练

容量规划不是“机器够不够”,而是“业务增长和流量模型变化时,核心链路是否还能维持 SLO”。

3.5.3.1 容量估算

容量估算从业务漏斗开始。

日活用户 -> 高峰活跃 -> 商品浏览 -> 加购 -> 结算 -> 下单 -> 支付

需要估算:

  • 峰值 QPS。
  • 并发请求数。
  • 数据写入量。
  • 热点 key 和热点商品。
  • 异步消息量。
  • 重试和补偿流量。
  • 外部依赖容量。

大促系统尤其要关注流量形态。日常流量通常平滑,大促流量可能在秒级集中爆发。平均 QPS 对大促没有意义。

3.5.3.2 压测

压测要验证三件事:

  1. 系统上限在哪里。
  2. 退化方式是否符合预期。
  3. 保护机制是否生效。

压测类型:

类型目标
单服务压测找服务自身瓶颈
全链路压测验证端到端能力
影子流量用真实流量分布验证读链路
限流压测验证限流阈值和用户体验
降级压测验证弱依赖失败时主链路是否稳定
恢复压测验证故障恢复后队列和补偿是否追平

压测报告不要只写“峰值 QPS”。至少要包含:

  • 安全水位。
  • 瓶颈资源。
  • P99 曲线。
  • 错误率拐点。
  • 下游依赖压力。
  • 扩容建议。
  • 降级开关验证结果。

3.5.3.3 故障演练

故障演练比压测更接近真实事故。

常见演练:

  • 关闭一个应用实例。
  • 让某个下游延迟升高。
  • 让 Redis 热 key 失效。
  • 让 Kafka 消费者停止。
  • 让支付渠道回调延迟。
  • 让数据库只读延迟升高。
  • 灰度版本返回错误码。
  • 配置中心下发错误配置并回滚。

演练后要复盘:

  • 告警是否及时。
  • 值班是否知道怎么处理。
  • 开关是否可用。
  • 回滚是否顺畅。
  • 数据是否恢复一致。
  • 文档是否准确。

未演练的预案,不应被视为可靠性能力。

3.5.4 数据恢复的底线能力:对账、补偿、备份与恢复

业务系统的可靠性最终会落到数据上。服务恢复但数据错了,事故并没有结束。

数据可靠性包括:

能力目标
幂等防止重复副作用
状态机防止非法状态推进
Outbox保证本地事务和消息发送可恢复
对账发现跨系统不一致
补偿自动或人工修复不一致
DLQ隔离无法自动处理的数据
备份防止数据不可逆丢失
恢复演练确认备份真的能恢复

备份不是可靠性终点,恢复才是。很多团队有备份,但从未演练恢复;真正事故发生时才发现备份缺字段、权限不可用、恢复耗时不可接受。

关键业务数据至少要定期演练:

  • 单表误删恢复。
  • 单订单修复。
  • 按时间点恢复。
  • 跨系统对账修正。
  • 消息重放。
  • 补偿任务重跑。

数据修复必须有审计。任何人工修复都应该记录操作人、原因、SQL 或工具参数、影响行数、审批单和验证结果。

3.5.5 发布可靠性:多数事故来自变更

线上事故很大比例来自变更:代码发布、配置变更、数据迁移、活动配置、扩容缩容、依赖升级。

发布可靠性要控制三个问题:

  1. 变更前能否发现风险。
  2. 变更中能否限制影响。
  3. 变更后能否快速回滚。

3.5.5.1 灰度发布

灰度发布建议按层推进:

开发环境 -> 测试环境 -> 预发环境 -> 单实例灰度 -> 小流量灰度 -> 分区域灰度 -> 全量

灰度期间必须观察:

  • 错误率。
  • P99 延迟。
  • 业务成功率。
  • 下游依赖耗时。
  • 新旧版本差异。
  • 日志错误码。

如果灰度只看进程是否启动成功,等于没有灰度。

3.5.5.2 配置变更

配置变更和代码发布一样危险,甚至更危险,因为它通常绕过测试。

高风险配置包括:

  • 限流阈值。
  • 降级开关。
  • 价格规则。
  • 营销规则。
  • 路由规则。
  • 支付渠道权重。
  • 分库分表配置。
  • 库存策略。

配置中心至少应支持:

  • 变更审批。
  • 灰度下发。
  • 版本记录。
  • 一键回滚。
  • 生效范围展示。
  • 变更事件写入监控和日志。

3.5.5.3 数据库变更

数据库变更尤其要谨慎。

常见原则:

  • 先兼容旧代码,再发布新代码,最后清理旧字段。
  • 大表 DDL 使用在线变更工具。
  • 回填任务限速,避免打爆主库。
  • 回填有断点续跑和校验。
  • 删除字段前至少观察一个版本周期。
  • 变更前准备回滚和恢复方案。

很多事故不是 SQL 写错,而是回填任务没有限速,导致主库延迟和线上接口超时。

3.6 数据与资金安全篇:恢复、对账、补偿与资损防控

业务系统真正困难的地方,不是让每一次调用都成功,而是在调用失败、消息丢失、回调乱序、数据延迟、人工误操作和外部系统不可控时,仍然能把业务状态拉回正确轨道。

电商系统尤其如此。一次下单会穿过商品、库存、营销、计价、订单、支付、履约、供应商、搜索、财务等多个系统。任何一个系统都只能保证自己局部正确,却很难在同一个事务里保证全链路同时成功。于是,系统必须接受一个事实:短时间不一致不可避免,长期不收敛不可接受

对账、补偿、DLQ 和故障恢复,就是让系统从“偶发错误靠人肉排查”走向“差异可发现、失败可隔离、修复可执行、过程可审计”的工程体系。

本章会以电商系统为主线,回答几个关键问题:

  1. 什么情况下必须做对账,谁是权威数据源?
  2. 对账发现差异后,如何生成安全的补偿任务?
  3. 什么错误应该自动重试,什么错误应该进入 DLQ?
  4. DLQ 为什么不能只是 Kafka 的死信 Topic?
  5. 支付成功但订单未更新、库存扣了但订单失败、供应商同步一半失败,应该如何恢复?
  6. 如何设计后台、监控、审计和人工介入流程,让故障恢复真正可运营?

3.6.1 先建立一个判断:恢复能力是主链路的一部分

很多团队把对账和补偿当作“上线后再补”的兜底脚本。这是危险的。业务系统上线第一天,就应该具备基本恢复能力。

因为故障不是例外,而是分布式协作的常态:

故障形态电商例子如果没有恢复能力
请求超时库存预占接口超时,但下游实际预占成功订单失败,库存长期占用
回调丢失支付渠道扣款成功,回调没有送达用户已扣款,订单仍待支付
消息失败商品已发布,搜索索引事件消费失败前台搜索不到商品或仍展示下架商品
消费乱序先收到订单取消,再收到支付成功事件状态机被错误推进
部分成功订单创建成功,优惠券锁定失败用户看到订单异常,券状态不一致
外部延迟供应商库存更新延迟 10 分钟平台继续售卖已售罄商品
人工误操作运营错误调整价格或库存需要回滚、冻结和审计

可靠的系统不是没有这些问题,而是问题出现后能够自动识别、自动收敛或可控地交给人工处理。

这一章讨论的对象不是单个技术组件,而是一条恢复闭环:

业务事实产生
  -> 事件投递 / 状态推进 / 外部交互
  -> 差异出现
  -> 对账发现差异
  -> 差异分类
  -> 自动补偿或进入 DLQ
  -> 人工修复或重新投递
  -> 再次对账确认收敛
  -> 形成审计记录和复盘材料

如果这个闭环缺任何一环,系统都会变成“线上看起来能跑,但出了问题谁也说不清”。

3.6.2 四个核心概念

3.6.2.1 对账

对账是用一个或多个权威数据源,比对另一个系统中的派生状态、投影状态或外部状态,发现差异并形成可处理的差异项。

对账不是财务系统专属概念。只要存在下面任意一种情况,就需要对账:

  • 跨系统异步同步。
  • 事件驱动的最终一致。
  • 本地系统与外部渠道协作。
  • 缓存、索引、读模型与主数据分离。
  • 主链路为了性能接受短窗口不一致。
  • 需要审计、追溯和证明业务结果。

例如:

对账场景权威数据源被对账对象常见差异
支付对账支付渠道账单、银行流水本地支付单、订单状态渠道成功,本地未支付
库存对账库存流水账本库存余额快照、Redis 库存流水扣减成功,余额未刷新
订单对账订单状态日志、支付事实订单主状态、履约状态支付成功但订单未推进
商品对账商品正式表、发布版本搜索索引、缓存、推荐特征商品下架但搜索仍可见
营销对账券实例状态、核销流水订单优惠快照、用户券包订单取消但券未释放
供应商对账供应商接口或文件平台商品、库存、价格供应商下架但平台仍售卖
消息对账Outbox 表Kafka 消费进度、下游处理表事件已生成但未被消费

对账的本质是:用更可信的事实校准更容易漂移的状态

3.6.2.2 补偿

补偿是针对已发现的不一致,执行一个可幂等、可重试、可审计的修复动作,让系统重新收敛。

补偿不是“反向操作”这么简单。真实业务里,补偿动作要遵守状态机、资金规则、库存规则和用户体验边界。

例如:

  • 库存预占成功但订单创建失败,可以释放库存。
  • 支付成功但订单仍待支付,应推进订单到已支付,而不是退款。
  • 订单已取消后又收到支付成功,可能要进入退款或人工确认,而不是直接把订单改回已支付。
  • 商品已下架但搜索仍展示,应删除搜索文档或标记不可售。
  • 供应商酒店价格异常,可能不能自动覆盖,要进入人工审核。

补偿的核心原则是:修复业务结果,而不是机械回滚技术动作

3.6.2.3 DLQ

DLQ 是 Dead Letter Queue,死信队列。但在业务系统里,DLQ 不应该只是 MQ 里的失败 Topic。

对于电商系统,失败数据往往需要查询、筛选、分派、修复、重新投递、忽略、审计和统计。Kafka DLQ 可以保留失败消息,但它不适合承担运营治理主存储。

更成熟的做法是:

Kafka DLQ:短期失败消息缓冲,可选
MySQL DLQ:权威问题单、处理状态、修复入口和审计台账
对象存储:保存大 payload、原始文件、渠道账单、供应商原始响应
运营后台:提供筛选、修复、重放、忽略、导出、审批

所以本章提到 DLQ 时,重点指“可运营的死信治理体系”,不只是消息中间件里的一个 Topic。

3.6.2.4 故障恢复

故障恢复是比对账、补偿、DLQ 更大的概念。它覆盖请求失败、任务失败、数据不一致、系统宕机、灾备切换和人工修复。

可以分为四层:

层次目标常见手段
请求级恢复单次请求失败后快速返回或安全重试超时、重试、幂等键、状态查询
任务级恢复异步任务失败后继续推进重试队列、补偿任务、Checkpoint、Worker Lease
数据级恢复多系统数据重新收敛对账、重放、差异修复、人工工单
系统级恢复服务或机房故障后恢复业务能力回滚、切流、降级、灾备、数据恢复

成熟系统不会把恢复责任压在某一层。只靠请求重试会放大故障,只靠夜间对账会延长用户影响,只靠人工工单会拖垮团队。

3.6.3 设计对账前,先定义“事实”和“口径”

对账最容易失败的原因,不是 SQL 写错,而是团队没有统一“谁说了算”。

3.6.3.1 权威数据源

不同业务域的权威事实不同:

业务对象权威事实不应作为权威的对象
支付是否成功渠道交易流水、本地支付单状态机前端支付页结果、同步预下单返回
订单是否有效订单主状态 + 状态变更日志搜索索引、客服备注
库存是否扣减库存流水账本、预占记录、确认记录Redis 当前值、前台展示库存
券是否核销券实例状态机、核销流水订单优惠展示字段
商品是否可售商品发布版本、可售规则、库存状态ES 文档、缓存详情页
供应商是否有库存供应商实时查询或最近一次有效同步平台缓存库存

权威数据源也不是永远固定的。资金事实通常以渠道和银行为准;业务状态以本地状态机为准;履约状态可能以供应商确认为准;财务入账以总账系统为准。对账系统要明确每类差异的裁决规则。

3.6.3.2 业务口径

同一个字段在不同系统里可能含义不同。

以支付为例:

字段可能的口径差异
paid_amount用户实付、渠道扣款、平台到账、扣除手续费后金额
paid_time用户确认时间、渠道扣款时间、回调到达时间、本地入库时间
success渠道受理成功、扣款成功、清算成功、可结算成功
refund_amount申请退款金额、渠道退款金额、实际到账金额

如果这些口径不统一,对账会不断制造“假差异”。因此重要字段必须有数据字典,并在系统边界上写清楚。

3.6.3.3 差异容忍窗口

最终一致系统允许短时间不一致。对账不能把所有延迟都当事故。

场景合理容忍窗口说明
支付回调到订单推进秒级到分钟级回调、Outbox、消费者可能有短暂延迟
商品发布到搜索可见分钟级索引刷新和缓存刷新需要时间
库存 Redis 与 MySQL 快照秒级到分钟级热路径与账本路径可能异步
供应商库存同步分钟级到小时级取决于供应商接口能力和品类风险
财务清结算T+1 或 T+N取决于渠道账单周期

对账规则必须写清“多长时间内算正常延迟,多长时间后算差异”。否则系统会在高峰期产生大量误报。

3.6.4 对账系统的分层设计

一套通用对账系统通常包含五层。

层次职责电商例子
数据采集层拉取权威数据和被对账数据渠道账单、订单表、库存流水、供应商文件
标准化层把异构数据转成统一模型金额单位、币种、状态枚举、时间格式
比对层执行 Join、聚合、恒等式校验支付单和渠道流水外连接
差异层生成差异项并分类长款、短款、状态不一致、缺失记录
处置层自动补偿、人工工单、忽略、复核推进订单、退款、释放库存、重建索引

不要把对账写成一个巨大定时脚本。脚本可以作为第一版实现,但架构上仍应具备批次、明细、差异状态、处理动作和审计。

3.6.4.1 对账批次

每次对账都应该有批次记录。

CREATE TABLE recon_batch (
    id BIGINT PRIMARY KEY,
    recon_type VARCHAR(64) NOT NULL,
    biz_date DATE NOT NULL,
    source_system VARCHAR(64) NOT NULL,
    target_system VARCHAR(64) NOT NULL,
    status VARCHAR(32) NOT NULL,
    total_count BIGINT NOT NULL DEFAULT 0,
    matched_count BIGINT NOT NULL DEFAULT 0,
    diff_count BIGINT NOT NULL DEFAULT 0,
    auto_fixed_count BIGINT NOT NULL DEFAULT 0,
    manual_required_count BIGINT NOT NULL DEFAULT 0,
    started_at DATETIME NOT NULL,
    finished_at DATETIME NULL,
    error_code VARCHAR(64) NULL,
    error_message VARCHAR(512) NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    UNIQUE KEY uk_recon_batch (recon_type, biz_date, source_system, target_system)
);

批次的价值是可追溯:今天对了什么,是否完成,发现多少差异,自动修复多少,还剩多少需要人工处理。

3.6.4.2 对账明细和差异项

差异项应单独建表,而不是只打日志。

CREATE TABLE recon_diff (
    id BIGINT PRIMARY KEY,
    batch_id BIGINT NOT NULL,
    recon_type VARCHAR(64) NOT NULL,
    biz_id VARCHAR(128) NOT NULL,
    biz_sub_id VARCHAR(128) NULL,
    diff_type VARCHAR(64) NOT NULL,
    severity VARCHAR(32) NOT NULL,
    source_snapshot JSON NOT NULL,
    target_snapshot JSON NOT NULL,
    expected_action VARCHAR(64) NOT NULL,
    status VARCHAR(32) NOT NULL,
    compensation_task_id BIGINT NULL,
    owner VARCHAR(64) NULL,
    first_seen_at DATETIME NOT NULL,
    last_seen_at DATETIME NOT NULL,
    resolved_at DATETIME NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    UNIQUE KEY uk_diff_dedup (recon_type, biz_id, biz_sub_id, diff_type)
);

几个关键点:

  • source_snapshottarget_snapshot 保存比对时的证据。
  • diff_type 用于分类,不要只写自然语言。
  • expected_action 说明建议处理动作。
  • status 管理差异生命周期,不能靠“有没有日志”判断是否处理。
  • 唯一键用于差异去重,避免同一问题每天生成一堆新差异。

3.6.4.3 差异分类

差异分类决定能否自动修复。

差异类型示例推荐动作
缺本地记录渠道有成功支付,本地无支付单人工确认或补建支付单,风险高
缺外部记录本地支付成功,渠道账单无记录查单复核,可能短款
状态不一致渠道成功,本地支付中自动查单后推进状态
金额不一致本地 100 元,渠道 99 元人工复核,禁止自动改账
时间延迟订单已支付,履约未开始超窗口后生成补偿
投影缺失商品已发布,ES 无文档自动重建索引
重复记录同一渠道流水命中多笔本地单P0 资损风险,人工冻结

原则上,资金金额差异、重复扣款、重复退款、跨用户归属错误,都不应该直接自动修复。可以自动生成工单、冻结风险对象、通知负责人,但不能让补偿程序“自作主张”。

3.6.5 电商核心场景对账

3.6.5.1 支付对账

支付对账是最典型、也最不能偷懒的对账场景。

核心数据源包括:

数据源作用
渠道账单外部扣款和退款事实
本地支付单平台支付状态机
支付渠道交互日志请求、回调、查单证据
订单表用户侧交易状态
财务分录入账和清结算口径

支付对账常见差异:

差异含义处理
渠道成功,本地支付中回调丢失或处理失败主动查单,推进支付单和订单
渠道成功,本地失败本地状态错误或晚到成功进入人工复核,可能需要补发事件
本地成功,渠道无成功记录本地误判成功或账单延迟查单复核,超窗口转人工
渠道退款成功,本地退款中退款回调丢失推进退款单,通知订单售后
金额不一致资损风险冻结差异项,人工复核

支付对账的推荐流程:

T+1 拉取渠道账单
  -> 校验文件完整性和签名
  -> 标准化为 ChannelTradeRow
  -> 与 payment_channel_log / payment_order 外连接
  -> 生成 recon_diff
  -> 对低风险状态差异自动查单
  -> 查单确认后生成 compensation_task
  -> 补偿推进支付单、订单、Outbox
  -> 高风险金额差异进入人工复核

注意:支付对账的自动补偿应该优先“推进状态”,而不是直接“改金额”。金额差异必须谨慎,因为它会影响财务、税务、商家结算和用户权益。

3.6.5.2 订单对账

订单是业务承诺的载体。订单对账重点不是“表里有没有数据”,而是订单状态是否和周边事实一致。

订单状态需要对齐的事实
待支付支付单应未成功,库存和营销资源仍在锁定窗口内
已支付支付成功事实存在,库存确认或履约创建应已触发
已取消支付未成功或已退款,库存和券应释放
已发货 / 履约中履约单或供应商订单应存在
已完成履约完成、售后窗口和财务确认满足规则

典型对账规则:

  • 支付单成功超过 1 分钟,订单仍待支付,生成订单推进补偿。
  • 订单取消超过 5 分钟,库存预占仍未释放,生成库存释放补偿。
  • 订单已支付超过 10 分钟,履约单未创建,生成履约补偿。
  • 订单已退款,营销券仍显示已核销,生成营销释放或人工修复。
  • 主单完成,但子单仍处于中间态,生成状态修复任务。

订单对账必须尊重状态机。不能因为“支付成功”就无条件把订单改成“已支付”。如果订单已经取消、退款、关闭或进入风控冻结,补偿动作应该进入人工确认或走专门的反向流程。

3.6.5.3 库存对账

库存对账的目标是保证“可售、预占、已售、释放、作废”这些状态最终符合账本。

一个常见库存恒等式:

total = available + reserved + sold + locked + damaged

如果使用 Redis 做热路径扣减,还需要额外比较:

MySQL 可售快照
  vs Redis 可售值
  vs 库存流水聚合
  vs 订单预占状态

典型差异:

差异可能原因处理
Redis 少于 MySQL扣减成功但落库失败、回填失败以账本为准重建 Redis
Redis 多于 MySQL释放重复、缓存覆盖错误冻结商品,按账本修复
预占超时未释放关单任务失败补偿释放预占
已支付订单库存仍 reserved支付确认事件消费失败补偿确认 sold
流水聚合不等于余额人工改库或程序缺陷停止自动修复,人工排查

库存对账最重要的原则是:Redis 不是账本。故障恢复时应以 MySQL 账本、预占记录、订单状态和审计日志为准,Redis 可以重建。

3.6.5.4 商品与搜索对账

商品系统经常采用“主数据 + 读模型”架构。正式商品表是权威数据,搜索索引、商品详情缓存、推荐特征都是投影。

常见对账规则:

  • 商品已下架,ES 仍可搜索到,删除或标记不可售。
  • 商品已发布,ES 缺文档,重建索引。
  • 商品价格版本已更新,详情页缓存仍是旧版本,刷新缓存。
  • 商品可售状态依赖库存和履约规则,搜索文档的可售标记过期,重新投影。
  • 供应商商品被标记风险,前台仍展示,强制下架并告警。

商品投影对账通常可以自动修复,因为它不改变主数据,只重建派生视图。但如果发现主数据本身质量异常,例如类目映射错误、酒店坐标漂移、价格异常波动,则应进入质量问题单或 DLQ,而不是自动发布。

3.6.5.5 供应商同步对账

供应商同步是电商系统里最容易产生长尾差异的地方。供应商接口可能限流、超时、字段缺失、分页游标异常、同一商品编码复用、价格库存延迟更新。

供应商对账要区分三类事实:

事实说明
Raw Snapshot供应商原始响应,便于追溯和重放
Standardized Model平台标准化后的中间模型
Published Version已发布到交易链路的正式版本

同步成功不等于可以发布。一个酒店从供应商拉取成功,但城市映射失败、经纬度漂移、价格低于风险阈值、房型缺关键字段,都不应该直接进入正式商品。

典型处理:

  • 网络超时:指数退避重试。
  • 供应商 5xx:限流、熔断、延迟重试。
  • 字段缺失:进入 MySQL DLQ,等待供应商或运营修复。
  • 城市映射失败:进入映射工单。
  • 价格异常:进入审核,不自动发布。
  • 下游索引刷新失败:生成投影补偿。

供应商同步的恢复能力通常依赖 Task + Checkpoint + Worker Lease + Raw Snapshot + DLQ。没有这些能力,长任务一旦中断,就只能从头跑,或者靠人工猜进度。

3.6.6 补偿任务设计

补偿任务不是简单的定时重试。它是一个小型工作流系统。

3.6.6.1 补偿任务模型

CREATE TABLE compensation_task (
    id BIGINT PRIMARY KEY,
    task_type VARCHAR(64) NOT NULL,
    biz_type VARCHAR(64) NOT NULL,
    biz_id VARCHAR(128) NOT NULL,
    biz_sub_id VARCHAR(128) NULL,
    priority INT NOT NULL DEFAULT 0,
    status VARCHAR(32) NOT NULL,
    idempotency_key VARCHAR(128) NOT NULL,
    request_payload JSON NOT NULL,
    context_snapshot JSON NOT NULL,
    retry_count INT NOT NULL DEFAULT 0,
    max_retry_count INT NOT NULL DEFAULT 10,
    next_retry_at DATETIME NOT NULL,
    last_error_code VARCHAR(64) NULL,
    last_error_message VARCHAR(512) NULL,
    locked_by VARCHAR(128) NULL,
    locked_until DATETIME NULL,
    source_type VARCHAR(64) NOT NULL,
    source_id BIGINT NULL,
    manual_required TINYINT NOT NULL DEFAULT 0,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    UNIQUE KEY uk_comp_idem (task_type, idempotency_key),
    KEY idx_comp_schedule (status, next_retry_at, priority),
    KEY idx_comp_biz (biz_type, biz_id)
);

字段说明:

字段设计目的
task_type区分支付查单、订单推进、库存释放、索引重建等动作
biz_type / biz_id关联业务对象,便于客服和运营检索
idempotency_key防止同一差异生成多个重复补偿
context_snapshot保存补偿创建时的上下文证据
source_type / source_id关联来源,例如 recon_diff、dlq、manual
locked_by / locked_until支持多 Worker 抢占和故障恢复
manual_required标记是否需要人工审批或处理

补偿任务必须能回答:为什么生成、要修什么、修复前证据是什么、谁执行过、执行结果如何。

3.6.6.2 补偿状态机

建议状态机保持简单:

PENDING
  -> RUNNING
  -> SUCCESS
  -> RETRY_WAIT
  -> MANUAL_REQUIRED
  -> DLQ
  -> CANCELLED

状态含义:

状态含义
PENDING等待执行
RUNNINGWorker 正在执行
RETRY_WAIT可重试失败,等待下一次执行
SUCCESS补偿完成,并通过必要校验
MANUAL_REQUIRED需要人工判断或审批
DLQ自动补偿已无法继续,转入死信治理
CANCELLED差异已消失或任务被人工取消

不要让任务在 RUNNING 状态永久停留。Worker 必须使用租约,超时后允许其他 Worker 接管。

3.6.6.3 重试策略

补偿重试要区分错误类型。

错误类型示例策略
瞬时错误网络抖动、连接重置指数退避重试
下游限流渠道 QPS 超限、供应商 429降低并发,延后重试
数据未就绪订单刚创建,支付单尚未落库短延迟重试
业务冲突状态机不允许推进转人工或重新对账
参数错误商品 ID 不存在、字段格式错误进入 DLQ
资损风险金额不一致、重复扣款冻结并人工复核

一个常见退避策略:

第 1 次:30 秒后重试
第 2 次:2 分钟后重试
第 3 次:10 分钟后重试
第 4 次:30 分钟后重试
第 5 次:2 小时后重试
超过阈值:MANUAL_REQUIRED 或 DLQ

不要无上限重试。无上限重试会掩盖真实问题,打爆下游,也会让补偿队列永远无法清空。

3.6.6.4 Worker 执行原则

补偿 Worker 应该遵守这些原则:

  1. 先抢租约,再执行任务。
  2. 执行前重新读取业务对象最新状态。
  3. 判断差异是否仍然存在。
  4. 调用幂等接口或执行条件更新。
  5. 执行后验证结果。
  6. 写补偿日志和审计记录。
  7. 失败时按错误分类更新下一次动作。

伪流程:

扫描 PENDING / RETRY_WAIT 且 next_retry_at <= now 的任务
  -> CAS 抢占 locked_by / locked_until
  -> 读取任务和业务对象
  -> 如果差异已消失,标记 SUCCESS 或 CANCELLED
  -> 如果状态机不允许自动修复,标记 MANUAL_REQUIRED
  -> 执行幂等补偿动作
  -> 校验修复结果
  -> 成功:标记 SUCCESS
  -> 可重试失败:计算 next_retry_at,标记 RETRY_WAIT
  -> 不可重试失败:写入 DLQ

3.6.6.5 补偿接口要幂等

补偿任务本身会重试,也可能被人工重放。因此所有补偿动作都必须幂等。

补偿动作幂等设计
推进订单已支付order_id + paid_event_id 唯一约束,状态条件更新
释放库存预占reservation_id + release_reason 唯一流水,状态机从 reserved 到 released
确认库存已售reservation_id 只能确认一次
释放优惠券coupon_lock_id 状态机,重复释放直接返回成功
重建搜索索引item_id + publish_version 覆盖式写入
支付查单payment_id 查询不产生副作用,结果推进用状态机
供应商重投递task_item_id + retry_round 去重,保留原始 payload 引用

判断幂等是否合格,可以问一句:同一补偿任务执行 10 次,业务结果是否仍然正确?

3.6.6.6 补偿不是万能的

有些差异不应该自动补偿。

场景原因
金额不一致可能涉及资损、税务、分账
重复支付需要确认是否退款、是否合并订单
跨用户归属错误可能是严重数据串号
商品价格异常可能需要法务和运营判断
供应商确认失败但用户已支付需要客服、退款或替代履约策略
人工 SQL 导致状态错乱需要冻结现场,防止越修越错

自动化的边界必须清楚。好的补偿系统不是“什么都自动修”,而是低风险自动收敛,高风险快速冻结、分派和审计。

3.6.7 DLQ 设计:从失败消息到可运营问题单

3.6.7.1 什么应该进入 DLQ

进入 DLQ 的前提是:系统判断当前数据无法继续正常自动处理,或者继续重试会产生更高风险。

常见进入 DLQ 的原因:

类型例子是否可自动恢复
格式错误消息 JSON 无法解析、字段类型不兼容通常不可
业务对象缺失订单不存在、商品不存在可能需要等待或人工
状态冲突订单已取消但收到支付成功事件需要业务判断
下游持续失败重试超过阈值可能稍后恢复
数据质量问题供应商城市映射失败、价格异常通常需要人工
幂等冲突同一幂等键参数不同高风险,人工
安全风险验签失败、来源不可信不应重放

不要把所有失败都立即丢进 DLQ。瞬时网络失败应该先重试;明确不可恢复的数据问题才应进入 DLQ。

3.6.7.2 MySQL DLQ 表

业务 DLQ 推荐落 MySQL 或其他可查询事务存储,作为权威问题单。

CREATE TABLE dead_letter_item (
    id BIGINT PRIMARY KEY,
    dlq_type VARCHAR(64) NOT NULL,
    biz_type VARCHAR(64) NOT NULL,
    biz_id VARCHAR(128) NOT NULL,
    stage VARCHAR(64) NOT NULL,
    error_code VARCHAR(64) NOT NULL,
    error_message VARCHAR(512) NOT NULL,
    retryable TINYINT NOT NULL DEFAULT 0,
    severity VARCHAR(32) NOT NULL,
    status VARCHAR(32) NOT NULL,
    payload_ref VARCHAR(512) NULL,
    payload_digest VARCHAR(128) NOT NULL,
    context_snapshot JSON NOT NULL,
    retry_count INT NOT NULL DEFAULT 0,
    next_retry_at DATETIME NULL,
    owner VARCHAR(64) NULL,
    resolved_by VARCHAR(64) NULL,
    resolved_at DATETIME NULL,
    resolution VARCHAR(64) NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    UNIQUE KEY uk_dlq_dedup (dlq_type, biz_type, biz_id, stage, error_code, payload_digest),
    KEY idx_dlq_status (status, severity, created_at),
    KEY idx_dlq_biz (biz_type, biz_id)
);

设计要点:

  • 大 payload 不要直接塞主表,保存对象存储引用和摘要。
  • payload_digest 用于去重和防篡改。
  • stage 表示失败阶段,例如 PARSEVALIDATEPUBLISHCONSUMECALLBACK
  • resolution 表示处理结果,例如 REPLAYEDIGNOREDMANUAL_FIXEDREFUNDED
  • 所有人工动作必须写审计日志。

3.6.7.3 DLQ 状态机

OPEN
  -> ASSIGNED
  -> RETRYING
  -> RESOLVED
  -> IGNORED
  -> ESCALATED
状态含义
OPEN新进入,等待处理
ASSIGNED已分派给 owner
RETRYING正在自动或人工重放
RESOLVED已修复并验证
IGNORED确认为无需处理,必须填写原因
ESCALATED升级到事故、客服、财务或供应商

DLQ 的“忽略”能力很重要,但必须有权限和审计。没有忽略能力,队列会永远堆积;忽略没有审计,又会变成掩盖问题的洞。

3.6.7.4 Kafka DLQ 与 MySQL DLQ 的边界

能力Kafka DLQMySQL DLQ
保留失败消息
顺序回放
运营查询
分派处理
人工修复
审计报表
大规模吞吐
问题单生命周期

推荐组合:

消息消费失败
  -> 可重试错误:延迟重试 Topic
  -> 超过阈值:写 Kafka DLQ 保留原消息
  -> 同时写 MySQL DLQ 作为问题单
  -> 运营或系统修复后,从 MySQL 触发重新投递
  -> 需要原始消息时,从 Kafka / 对象存储读取 payload

3.6.7.5 重新投递要有护栏

DLQ 重新投递不能只是一个按钮。

重放前必须检查:

  • 原始 payload 是否完整。
  • 消费代码是否已经修复。
  • 业务对象当前状态是否允许重放。
  • 是否会重复扣款、重复发券、重复扣库存。
  • 是否需要灰度重放。
  • 是否需要限速。
  • 是否需要审批。

重放策略:

策略使用场景
单条重放高风险订单、支付、退款
批量限速重放商品索引、缓存刷新
按错误码重放已修复某类解析错误
按时间窗口重放某次发布造成的失败
影子重放先验证新代码能否处理,不写正式状态

高风险 DLQ 应该要求双人审批。尤其是支付、退款、库存和营销权益,不能让一个人随手批量重放。

3.6.8 Outbox、事件重放与对账的关系

Outbox 解决的是“本地事务成功后,事件可靠发出去”的问题。对账解决的是“事件发出去之后,下游最终有没有处理成功”的问题。

两者不能互相替代。

能力解决问题不能解决
Outbox本地数据和事件生成同事务下游消费失败、业务处理失败
MQ 重试临时消费失败不可重试数据错误
DLQ隔离无法处理的数据自动判断业务真相
对账发现最终状态差异实时阻止所有错误
补偿修复已发现差异替代主链路正确性

一个电商订单支付成功事件的可靠链路应该是:

支付回调落库
  -> 支付单状态机推进为 SUCCESS
  -> 同事务写 payment_outbox_event
  -> Outbox Relay 投递 PaymentSucceeded
  -> 订单服务消费事件,幂等推进订单
  -> 履约、库存、营销继续消费订单事件
  -> 对账任务扫描支付成功但订单未支付
  -> 发现差异后生成订单推进补偿

即使 Outbox 做得很好,对账仍然必要。因为下游可能消费失败、业务状态冲突、代码 Bug、数据污染或人工误操作。

3.6.9 端到端案例一:支付成功但订单未更新

这是电商系统最典型的恢复案例。

3.6.9.1 故障过程

用户提交支付
  -> 支付渠道扣款成功
  -> 渠道回调支付服务
  -> 支付服务验签成功,更新支付单 SUCCESS
  -> 写 Outbox 事件 PaymentSucceeded
  -> 订单服务消费事件时数据库超时
  -> MQ 重试多次失败
  -> 消息进入 DLQ
  -> 用户订单仍显示待支付

用户看到的是“钱扣了,订单没变”。这类问题必须优先处理,因为它直接影响信任。

3.6.9.2 发现方式

至少应该有三条发现路径:

  1. 实时链路告警:支付成功事件消费失败率升高。
  2. 近实时对账:支付成功超过 1 分钟,订单仍待支付。
  3. T+1 渠道对账:渠道成功,本地订单未完成。

只靠用户投诉是不合格的。

3.6.9.3 自动补偿策略

低风险情况下,可以自动推进订单:

前置条件判断
支付单为 SUCCESS
渠道流水验签和金额正确
订单仍处于 PendingPay
订单未取消、未退款、未风控冻结
支付金额等于订单应付金额
幂等事件未处理成功

补偿动作:

创建 compensation_task: ADVANCE_ORDER_PAID
  -> 读取订单和支付单最新状态
  -> 校验金额、币种、支付单归属
  -> CAS 更新订单 PendingPay -> Paid
  -> 写订单状态日志
  -> 写 OrderPaid Outbox
  -> 标记补偿成功
  -> 对账差异改为 RESOLVED

如果订单已取消,就不能简单推进。需要判断取消原因和支付时间:

情况处理
支付发生在取消前可能是状态处理延迟,人工确认
支付发生在取消后通常发起退款或人工确认
订单已风控冻结进入风控工单
金额不一致进入财务差异工单

3.6.9.4 用户体验

恢复系统还要考虑用户展示:

  • 支付成功但订单未推进时,不要长期显示“待支付”。
  • 可以展示“支付结果确认中”,并提供刷新或客服入口。
  • 如果自动补偿完成,应同步推送订单状态。
  • 如果需要退款,应明确退款处理中和预计到账时间。

很多系统后台补偿做得不错,但前台状态表达很差,用户仍然会投诉。故障恢复不只是数据修复,也包括用户可解释性。

3.6.10 端到端案例二:库存预占成功但订单创建失败

3.6.10.1 故障过程

用户提交订单
  -> 订单服务调用库存预占
  -> 库存系统写 reservation SUCCESS
  -> 订单服务写订单主表时失败
  -> 用户收到下单失败
  -> 库存仍被占用

如果不处理,库存会被幽灵订单占住,造成少卖。

3.6.10.2 恢复设计

库存预占必须有过期时间和业务关联。

关键字段:

字段说明
reservation_id预占 ID
request_id创单请求幂等键
order_id如果订单创建成功则绑定
sku_id库存对象
quantity预占数量
statusRESERVED、CONFIRMED、RELEASED、EXPIRED
expire_at预占过期时间

恢复路径:

  1. 创单失败后,订单服务同步调用库存释放。
  2. 如果同步释放失败,写补偿任务 RELEASE_INVENTORY_RESERVATION
  3. 库存系统定时扫描超时 RESERVED 记录。
  4. 对账任务扫描“无订单绑定的有效预占”。
  5. 补偿释放库存并写释放流水。

注意:释放库存必须幂等。重复释放不能让库存加两次。

3.6.10.3 什么时候不能自动释放

如果订单服务写主表超时,可能订单实际已经写成功,只是响应失败。因此释放前要查订单:

查询结果处理
订单不存在可以释放
订单存在且待支付不释放,绑定 reservation
订单存在且已支付确认库存 sold
订单存在但状态异常转人工或重新对账

这就是为什么补偿执行前必须重新读取最新状态。补偿不能只相信任务创建时的上下文。

3.6.11 端到端案例三:供应商同步失败进入 DLQ

3.6.11.1 场景

一个酒店供应商每天同步 100 万个酒店和房型。某次同步中,供应商把城市编码从 BKK 改成新编码,但没有提前通知平台。平台城市映射失败,导致部分酒店无法发布。

如果系统只是打印日志,运营不会知道哪些酒店失败,也无法修复。

3.6.11.2 正确链路

Supplier Sync Task
  -> 拉取 Raw Snapshot
  -> 标准化字段
  -> 城市 / 商户 / 品牌映射
  -> 质量校验
  -> Diff
  -> 低风险自动发布
  -> 高风险审核
  -> 失败进入 MySQL DLQ

DLQ 记录应包含:

字段示例
dlq_typeSUPPLIER_SYNC
biz_typeHOTEL
biz_idsupplier_hotel_id
stageMAPPING
error_codeCITY_MAPPING_NOT_FOUND
payload_refraw snapshot 地址
context_snapshotsupplier_id、city_code、hotel_name、task_id
ownersupply-ops
retryablefalse

运营后台应该能按供应商、城市、错误码筛选,批量建立映射规则,然后重放受影响 DLQ。

3.6.11.3 为什么不能直接自动发布

城市映射失败不是技术瞬时错误,而是业务语义缺失。自动发布可能导致:

  • 酒店出现在错误城市。
  • 用户搜索结果错误。
  • 税费和履约规则错误。
  • 供应商结算主体错误。

所以这类 DLQ 的正确处理不是重试,而是修复映射数据后重新投递。

3.6.11.4 重放后的验证

重放成功不等于结束。还要验证:

  • 商品正式版本是否更新。
  • 搜索索引是否刷新。
  • 详情页缓存是否刷新。
  • 可售诊断是否通过。
  • 质量报表中的失败数是否下降。
  • 原 DLQ 是否标记 RESOLVED 并写审计。

这才是闭环。

3.6.12 故障恢复中的人工介入

业务系统不能完全排斥人工介入。关键是人工介入必须产品化、权限化、审计化。

3.6.12.1 人工介入入口

建议后台至少提供:

能力说明
差异查询按订单号、支付单、SKU、供应商、错误码查找
证据展示展示对账快照、原始 payload、trace、日志链接
操作建议系统给出推荐动作和风险提示
单条修复对单个差异执行推进、释放、重建、退款等动作
批量处理对低风险同类问题批量重放
审批流程高风险动作需要双人审批
操作审计记录操作人、原因、参数、前后状态
结果验证操作后自动触发对账或状态检查

禁止直接让运营或研发用 SQL 改核心状态。手工 SQL 最大的问题不是慢,而是不可审计、不可复用、不可防错。

3.6.12.2 人工动作也要幂等

人工点击“重新投递”也可能点两次,后台超时后也可能重复提交。所以人工操作不能绕过幂等。

每次人工动作都应该生成 manual_operation 记录:

CREATE TABLE manual_operation (
    id BIGINT PRIMARY KEY,
    operation_type VARCHAR(64) NOT NULL,
    biz_type VARCHAR(64) NOT NULL,
    biz_id VARCHAR(128) NOT NULL,
    idempotency_key VARCHAR(128) NOT NULL,
    operator VARCHAR(64) NOT NULL,
    reason VARCHAR(512) NOT NULL,
    request_payload JSON NOT NULL,
    before_snapshot JSON NOT NULL,
    after_snapshot JSON NULL,
    status VARCHAR(32) NOT NULL,
    approved_by VARCHAR(64) NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    UNIQUE KEY uk_manual_idem (operation_type, idempotency_key)
);

人工修复不是“绕过系统”,而是通过系统提供的安全入口完成修复。

3.6.13 监控与指标

第 3 章已经系统讨论监控体系,本章只列对账、补偿和 DLQ 必须具备的专项指标。

3.6.13.1 对账指标

指标含义
recon_batch_success_rate对账批次成功率
recon_batch_duration_seconds对账耗时
recon_diff_total差异数量
recon_diff_age_seconds差异存活时间
recon_auto_fix_success_rate自动修复成功率
recon_manual_required_total需要人工处理的差异

关键告警:

  • 支付对账批次失败。
  • P0 差异数量大于 0,例如重复扣款、金额不一致。
  • 差异超过 SLA 未处理。
  • 同一错误码差异突然上升。

3.6.13.2 补偿指标

指标含义
compensation_pending_total待执行任务数
compensation_retry_total重试任务数
compensation_success_rate补偿成功率
compensation_oldest_age_seconds最老任务年龄
compensation_manual_required_total需要人工介入数量
compensation_execution_duration_seconds单次执行耗时

比“失败数量”更重要的是“最老任务年龄”。如果一条支付补偿卡了 6 小时,哪怕总数只有 1,也可能是严重用户问题。

3.6.13.3 DLQ 指标

指标含义
dlq_open_total未处理 DLQ 数
dlq_new_total新增 DLQ 数
dlq_resolved_total已解决数
dlq_replay_success_rate重放成功率
dlq_oldest_age_seconds最老 DLQ 年龄
dlq_by_error_code按错误码分布
dlq_by_owner按负责人分布

DLQ 要有运营 SLA。例如:

  • 支付和退款 DLQ:15 分钟内响应。
  • 订单状态 DLQ:30 分钟内响应。
  • 商品投影 DLQ:2 小时内处理。
  • 供应商数据质量 DLQ:工作日内处理或升级供应商。

3.6.14 上线前检查清单

上线一个跨系统链路前,可以用下面清单检查恢复能力。

检查项必须回答
权威数据源出现差异时谁说了算
幂等键重试、回调、补偿是否共用稳定幂等语义
状态机哪些状态允许自动推进,哪些必须人工
Outbox本地事务成功后事件是否可靠投递
对账规则是否有差异发现机制和容忍窗口
补偿任务是否有任务表、状态机、租约、重试和审计
DLQ不可自动处理的数据是否可查询、可分派、可重放
人工入口是否禁止直接 SQL 修核心状态
监控告警是否能看到差异数量、积压时间、重放成功率
Runbook值班同学是否知道如何止血和恢复

如果一个链路没有对账和补偿,它不是“设计简单”,而是把复杂度转嫁给事故当天的 on-call。

3.6.15 常见反模式

反模式问题
只写日志不落差异表问题不可运营,无法统计和追踪
对账只告警不修复告警越来越多,团队逐渐麻木
补偿没有幂等重试本身制造二次事故
所有失败都无限重试打爆下游,掩盖数据问题
DLQ 只是 Kafka Topic运营无法查询、分派、修复、审计
人工直接改库无权限边界、无审计、无防错
金额差异自动修极易扩大资损
不保存原始 payload无法复盘、无法重放、无法举证
不区分错误类型可重试和不可重试混在一起
补偿执行前不读最新状态修复旧问题,制造新问题

3.6.16 小结:恢复体系不是兜底脚本,而是业务可信度的一部分

对账、补偿、DLQ 与故障恢复,是业务系统长期稳定运行的底座。

对电商系统来说:

  • 支付对账保护资金事实。
  • 库存对账保护可售承诺。
  • 订单对账保护用户状态。
  • 商品与搜索对账保护前台体验。
  • 供应商对账保护供给质量。
  • 补偿任务负责把低风险差异自动拉回正确状态。
  • DLQ 负责把不可自动处理的问题变成可运营、可追踪、可审计的问题单。
  • 人工介入不是失败,而是高风险业务恢复的一部分,但必须通过系统化入口完成。

真正成熟的恢复体系,不是“出了问题有脚本”,而是从主链路设计时就把失败纳入模型:每个动作都有幂等语义,每个状态都有推进规则,每个事件都有重放路径,每个差异都有处理责任,每次人工操作都有证据链。

系统会失败,但业务结果必须最终可信。这就是对账、补偿、DLQ 和故障恢复的工程价值。

3.6.17 延伸思考:恢复能力为什么总在团队最忙时才显得昂贵

恢复体系的建设往往最容易被延后,因为它很少直接带来新用户、新 GMV 或新功能。但从长期看,恢复能力不足带来的代价,通常会以更昂贵的方式回到团队面前:一次支付差异、一次库存冻结、一次批量修复、一次通宵对账、一次无法解释的用户投诉。

真正成熟的团队,不会把对账、补偿、DLQ 和人工修复看成附属能力,而是会把它们视为主链路可信度的组成部分。因为系统规模越大,越不可能靠“所有调用都成功”维持正确,只能靠“失败后仍能收敛”维持业务可信。

3.6.18 资损防控:资金、库存、优惠与账务安全

资损防控是业务系统可靠性里最“硬”的部分。接口慢一点,用户可能刷新;推荐失败,用户可能少点一个商品;但一旦出现重复扣款、错误退款、超卖、优惠券被薅、商家错结算、价格算错,系统损失的就不只是可用性,而是现金、库存、商誉和合规风险。

很多团队把资损防控理解成支付系统或财务系统的事情,这是不完整的。真正的资损往往发生在业务链路交界处:计价给了错误金额,订单保存了错误快照,库存释放重复,营销券重复核销,支付回调乱序,退款绕过原路退回,供应商履约失败但订单仍完成,运营后台误改了高风险配置。

本章从业务系统工程视角讨论资损防控:如何识别资损场景,如何设计账本与状态机,如何在主链路中防错,如何在高风险操作前审批和拦截,如何通过对账、补偿、冻结和审计把损失控制在最小范围。

3.6.18.1 什么是资损

资损不只是“钱少了”。只要系统错误导致平台、商家、用户、供应商之间的权益分配偏离真实业务承诺,都可以视为资损风险。

在电商系统中,常见资损对象包括:

对象资损表现例子
现金多扣、少扣、漏扣、错退用户支付 100 元,订单只入账 90 元
库存超卖、少卖、重复释放、错误锁定库存只有 1 件,卖出 2 件
优惠重复用券、超预算补贴、错误叠加满减和新人券错误叠加导致 0 元购
积分 / 余额重复发放、重复扣减、扣减失败订单取消后积分未退回
商家结算错分账、错费率、错周期商家应结 1000 元,只结 900 元
供应商结算平台订单与供应商单不一致供应商已确认,平台未生成结算
发票 / 税务金额口径错误含税价和不含税价混用
履约权益虚拟码重复发、酒店错订同一券码发给两个用户

资损防控的目标不是让所有风险变成零。成本、体验和效率决定了系统只能对不同风险分级治理。核心目标是:

  1. 高风险错误尽量在发生前拦截。
  2. 已发生的差异尽快发现。
  3. 发现后能冻结影响范围。
  4. 修复动作可幂等、可审批、可审计。
  5. 损失金额、影响用户和责任边界可解释。

3.6.18.2 资损风险的来源

资损通常不是单点 Bug,而是业务规则、分布式一致性、人工操作和外部依赖共同作用的结果。

3.6.18.2.1 计价错误

计价错误是资损高发区。

场景风险
优惠叠加顺序错误折扣、满减、券、积分重复优惠
金额精度错误浮点计算、舍入规则不一致
币种转换错误汇率、币种精度、税费口径错误
价格缓存过期下架价、活动价、供应商价未刷新
运费 / 税费漏算用户少付或商家少收
会员价与活动价冲突高优先级规则未生效

计价资损最危险的地方在于:错误金额一旦进入订单快照,后续支付、退款、结算都会沿着错误口径继续执行。

3.6.18.2.2 状态机错误

状态机错误会让系统在不该推进时推进,或者在该回滚时没有回滚。

场景风险
支付成功晚于订单取消订单状态和资金事实冲突
退款成功但订单仍完成商家结算多结
发货后重复退款用户拿到商品又拿到钱
订单取消后库存未释放少卖
订单取消后券未退回用户权益损失
售后关闭后退款回调到达重复状态推进

状态机必须用代码和数据库约束共同保护,不能只靠业务同学“按流程操作”。

3.6.18.2.3 幂等缺失

高并发系统中,重复请求、重复回调、重复消息、重复人工提交都很常见。

幂等缺失会直接造成:

  • 重复创建订单。
  • 重复扣库存。
  • 重复核销优惠券。
  • 重复发放积分。
  • 重复退款。
  • 重复给供应商下单。

只在前端按钮上做防抖没有意义。资损防控要求服务端、数据库唯一键、状态机和流水账本共同保证幂等。

3.6.18.2.4 异步链路不一致

为了性能,电商系统大量使用异步事件。

支付成功
  -> 订单已支付
  -> 库存确认已售
  -> 营销确认核销
  -> 履约创建
  -> 商家结算计提
  -> 数据仓库入账

任何一步事件丢失、消费失败、乱序或重复,都可能造成资损。因此异步链路必须具备 Outbox、消费幂等、DLQ、对账和补偿。

3.6.18.2.5 人工操作

很多严重资损来自人工操作,而不是代码 Bug。

操作风险
批量改价大量商品低价售卖
修改活动规则优惠门槛错误、预算失控
手工补券重复发券或发错用户
手工退款超额退款或错退
直接 SQL 修数据绕过状态机和审计
修改支付渠道权重走错费率或不可用渠道
修改供应商映射商品履约和结算主体错误

资损防控必须把后台、配置中心、批量任务和人工修复工具纳入治理,而不是只保护 C 端接口。

3.6.18.2.6 外部系统不确定性

支付渠道、供应商、物流、银行、清结算文件都可能不稳定。

典型风险:

  • 渠道同步返回成功,但最终扣款失败。
  • 渠道回调成功,但对账文件缺记录。
  • 供应商确认成功,但后续取消。
  • 供应商价格和平台价格不同步。
  • 银行清算金额与本地账务不同。

外部依赖不能被当作本地事务的一部分。必须用适配层、状态机、查单、对账和人工复核治理。

3.6.18.3 资损防控的三道防线

成熟的资损防控不是某一个风控规则,而是三道防线。

防线目标手段
事前防错不让高风险动作发生规则校验、状态机、唯一键、限额、审批、灰度
事中拦截异常发生时立即阻断扩散风险规则、冻结、熔断、预算水位、异常检测
事后收敛已发生差异可发现、可修复对账、补偿、DLQ、审计、复盘

很多团队只做事后对账,等财务发现差异时,用户已经拿到错误权益,商家已经结算,供应商已经履约,修复成本极高。真正有效的资损防控必须前移。

3.6.18.3.1 事前防错

事前防错的核心是把业务不变量写进系统。

例如:

  • 退款总额不能超过支付成功金额。
  • 同一支付渠道流水只能绑定一笔支付单。
  • 同一张券只能被一个订单核销一次。
  • 库存释放必须关联原始预占流水。
  • 订单金额必须等于商品金额、优惠、运费、税费的可解释组合。
  • 活动预算消耗不能超过预算上限。
  • 高风险价格变更必须审批和灰度。

这些规则不能只存在于 PRD 或口头约定中,必须落到数据库约束、领域服务校验、审批流和监控规则。

3.6.18.3.2 事中拦截

事中拦截关注“异常趋势刚出现时,能否快速止血”。

典型拦截:

  • 某活动优惠核销速度超过预算曲线,自动暂停活动。
  • 某 SKU 下单价低于成本价阈值,自动下架或进入审核。
  • 某支付渠道短时间失败率异常,自动降权或切流。
  • 某商家退款率异常升高,冻结自动退款。
  • 某接口重复退款请求异常,触发用户或订单级限流。
  • 库存超卖风险出现,冻结商品可售。

拦截动作必须区分用户体验和资损风险。推荐失败可以降级,但金额异常应该宁可阻断,也不能带病放行。

3.6.18.3.3 事后收敛

事后收敛依赖第 3 章的对账、补偿、DLQ 和故障恢复能力。

关键要求:

  • 差异能被批次化发现。
  • 差异能按风险等级分类。
  • 低风险差异能自动修复。
  • 高风险差异能冻结并进入人工复核。
  • 所有修复动作都有审计证据。
  • 修复后再次对账确认收敛。

事后收敛不是失败的借口,而是承认分布式系统不可能永远同步成功。

3.6.18.4 资损防控的核心模型:账本、状态机、快照

资损防控有三个底层模型:账本、状态机、快照。

3.6.18.4.1 账本

账本记录每一次权益变化的事实流水。余额、库存、券、积分、商家结算都应该有账本。

账本的特点:

  1. 追加写,不随意物理删除。
  2. 每条流水有业务来源和幂等键。
  3. 借贷或增减方向明确。
  4. 能从流水重放出余额。
  5. 能与快照和外部账单对账。

以资金账本为例:

CREATE TABLE fund_ledger (
    id BIGINT PRIMARY KEY,
    account_id VARCHAR(128) NOT NULL,
    biz_type VARCHAR(64) NOT NULL,
    biz_id VARCHAR(128) NOT NULL,
    direction VARCHAR(16) NOT NULL,
    amount BIGINT NOT NULL,
    currency VARCHAR(16) NOT NULL,
    ledger_type VARCHAR(64) NOT NULL,
    idempotency_key VARCHAR(128) NOT NULL,
    before_balance BIGINT NULL,
    after_balance BIGINT NULL,
    status VARCHAR(32) NOT NULL,
    created_at DATETIME NOT NULL,
    UNIQUE KEY uk_fund_ledger_idem (ledger_type, idempotency_key),
    KEY idx_fund_ledger_account (account_id, created_at),
    KEY idx_fund_ledger_biz (biz_type, biz_id)
);

金额建议使用最小货币单位的整数,例如分、厘、cent,避免浮点误差。

3.6.18.4.2 状态机

状态机定义业务对象能从哪里到哪里。

支付单状态机示例:

INIT
  -> PAYING
  -> SUCCESS
  -> FAILED
  -> CLOSED
  -> REFUNDING
  -> REFUNDED

状态机要做三件事:

  1. 限制非法迁移。
  2. 处理重复事件。
  3. 为补偿和对账提供判断依据。

例如支付单从 SUCCESS 不能回到 PAYING;退款成功回调重复到达时应该幂等返回;订单已取消后收到支付成功事件时不能无条件推进订单,需要进入特殊处理。

3.6.18.4.3 快照

快照用于固定业务承诺。

订单创建时应保存:

  • 商品快照。
  • SKU 快照。
  • 价格快照。
  • 优惠快照。
  • 运费和税费快照。
  • 履约规则快照。
  • 供应商报价和确认快照。

如果订单只保存商品 ID,后续商品改价、活动下线、供应商规则变化,就无法解释当时为什么收这个价,也无法正确退款、结算和审计。

资损防控里有一个重要原则:交易发生后,用交易快照解释历史,不用当前配置解释历史。

3.6.18.5 计价资损防控

计价是资损防控第一现场。

3.6.18.5.1 金额公式必须可解释

订单应付金额应该能被公式解释:

payable_amount
  = item_amount
  + shipping_fee
  + tax_amount
  + service_fee
  - platform_discount
  - merchant_discount
  - coupon_discount
  - points_discount

每一项都必须有来源、版本和上限。

金额项防控要求
商品金额绑定商品价格版本和币种
运费保存运费模板版本和计算输入
税费保存税率、税区、含税口径
平台优惠绑定活动 ID、预算、规则版本
商家优惠区分商家承担和平台承担
券优惠绑定券实例和核销流水
积分抵扣保存积分扣减流水

如果金额只保存一个总价,后续退款、结算、财务入账都会变成猜谜。

3.6.18.5.2 金额不变量

计价系统必须校验不变量:

不变量说明
应付金额不能小于 0除非明确支持 0 元单
优惠总额不能超过可优惠金额防止倒贴
平台补贴不能超过活动预算防止预算穿透
商家承担金额不能超过商家授权防止错扣商家
退款金额不能超过原支付成功金额防止超退
分账总额不能超过实收金额防止商家多结
不同币种不能直接加减必须先明确汇率和结算币种

这些校验应该存在于计价服务、订单创建、支付创建、退款创建和结算生成多个关键点。

3.6.18.5.3 价格保护

价格变更要有风险保护:

  • 价格低于成本价或历史均价一定比例,进入审核。
  • 批量改价影响 SKU 数超过阈值,需要双人审批。
  • 活动价生效前灰度到小流量或内部用户。
  • 新规则上线后监控订单平均折扣率、毛利率和 0 元单数量。
  • 价格配置变更写审计,并能快速回滚。

价格错误通常扩散非常快。一个错误活动配置可能几分钟内产生几万笔订单,所以价格保护必须具备自动暂停能力。

3.6.18.6 订单、支付与退款资损防控

3.6.18.6.1 创建支付单

创建支付单前必须校验:

校验目的
订单仍可支付防止取消后支付
支付金额等于订单应付金额防止篡改金额
支付币种一致防止币种错配
支付单幂等防止重复支付单
订单未进入退款或风控冻结防止状态冲突

支付金额不能信任前端传入。前端只能提交订单号和支付方式,金额必须由服务端根据订单快照读取。

3.6.18.6.2 支付回调

支付回调是资损高风险入口。

处理顺序建议:

接收回调
  -> 验签
  -> 校验渠道商户号
  -> 幂等检查渠道流水
  -> 查询本地支付单
  -> 校验金额、币种、订单归属
  -> 状态机推进
  -> 写支付流水和 Outbox
  -> 返回渠道成功

几个关键原则:

  • 验签失败不能进入业务逻辑。
  • 渠道流水号必须唯一。
  • 回调金额必须等于支付单金额。
  • 更新支付状态和写 Outbox 应在同一事务内。
  • 成功响应渠道应该晚于本地事务提交。
  • 重复回调必须幂等返回成功。
3.6.18.6.3 退款防控

退款比支付更容易出资损,因为它常常由售后、客服、运营或自动规则触发。

退款前必须校验:

校验说明
原支付成功未支付不能退款
可退金额累计退款不能超过实付
退款原因是否符合售后规则
订单状态已发货、已核销、已完成的退款规则不同
权益回收券、积分、库存、发票是否需要反向处理
风险审批大额退款、批量退款、跨账户退款必须审批

退款建议使用退款单状态机:

INIT
  -> REFUNDING
  -> SUCCESS
  -> FAILED
  -> CLOSED

原路退回是默认原则。任何非原路退款、线下退款、人工补偿,都应提高风险等级并要求审批。

3.6.18.6.4 重复退款防控

重复退款常见来源:

  • 用户重复提交售后。
  • 客服重复点击。
  • 渠道回调重复。
  • 补偿任务重复执行。
  • 多个售后单同时关联同一支付单。

防控方式:

  • payment_id + refund_request_id 唯一。
  • 支付单累计退款金额使用条件更新。
  • 退款成功流水唯一。
  • 退款补偿幂等。
  • 售后单和退款单一对一或明确多对一规则。

金额更新建议使用条件约束:

UPDATE payment_order
SET refunded_amount = refunded_amount + :refund_amount
WHERE payment_id = :payment_id
  AND paid_amount - refunded_amount >= :refund_amount;

更新影响行数为 0 时,不能继续发起渠道退款。

3.6.18.7 库存与履约资损防控

库存资损既可能是超卖,也可能是少卖。

3.6.18.7.1 库存账本

库存系统不应该只有一个 available_stock 字段。至少需要库存流水:

流水类型说明
INIT初始化库存
RESERVE下单预占
CONFIRM支付成功确认已售
RELEASE取消或超时释放
ADJUST人工调整
FREEZE风险冻结
UNFREEZE解除冻结

库存余额可以是快照,但库存真相应来自流水和状态机。

3.6.18.7.2 超卖防控

超卖防控要覆盖并发、缓存和供应商三类风险。

风险防控
并发扣减DB 条件更新、Redis Lua、库存分片
重复扣减预占流水幂等键
重复释放释放流水唯一键
Redis 与 DB 不一致定期对账,以账本修复缓存
热点 SKU限流、排队、库存分桶
供应商库存延迟可售水位、安全库存、实时确认

典型扣减条件:

UPDATE inventory_balance
SET available = available - :qty,
    reserved = reserved + :qty
WHERE inventory_key = :inventory_key
  AND available >= :qty;

影响行数为 0 表示库存不足,不能继续创建可支付订单。

3.6.18.7.3 少卖防控

很多团队只关注超卖,忽略少卖。少卖也是资损,因为平台损失交易机会。

少卖来源:

  • 订单失败后库存未释放。
  • Redis 扣减成功但 DB 释放失败。
  • 预占过期扫描任务挂掉。
  • 供应商库存恢复后平台未同步。
  • 风险冻结后没有自动解冻。

少卖防控依赖:

  • 预占过期释放。
  • 库存对账。
  • 冻结库存监控。
  • 供应商库存新鲜度监控。
  • 手工冻结工单 SLA。
3.6.18.7.4 虚拟商品和券码

券码、充值、酒店、机票这类库存更复杂。

品类风险防控
券码同一码发给多个用户code_id 状态机、发码幂等
充值供应商扣款但用户未到账供应商请求幂等、回调对账
酒店平台收款但供应商未确认支付前二次确认或支付后快速取消退款
机票价格和舱位实时变化锁价、验舱、支付时效

虚拟履约必须有供应商请求流水,不能只记录“履约成功 / 失败”。否则供应商争议时无法举证。

3.6.18.8 营销与优惠资损防控

营销系统是“有意让利”的系统,也是资损高发区。

3.6.18.8.1 优惠预算

每个活动都应该有预算控制:

预算类型说明
总预算活动最多补贴金额
日预算单日最多补贴金额
用户预算单用户最多领取或核销
商家预算单商家承担上限
商品预算单 SKU 补贴上限
渠道预算不同渠道独立预算

预算扣减要使用原子操作,并有预算流水。

预算冻结
  -> 订单支付成功确认消耗
  -> 订单取消释放预算
  -> 退款时按规则返还或不返还

不要只在活动开始前校验预算,活动运行中必须实时监控预算消耗速度。

3.6.18.8.2 优惠叠加规则

优惠叠加要明确:

  • 哪些优惠互斥。
  • 哪些优惠可叠加。
  • 叠加顺序是什么。
  • 每类优惠作用范围是什么。
  • 优惠是否参与退款。
  • 优惠由平台还是商家承担。

常见事故是优惠规则从“单券”扩展到“多券 + 满减 + 积分 + 会员价”后,没有重新设计规则引擎,只是在代码里堆条件。

3.6.18.8.3 薅羊毛防控

营销资损不一定来自 Bug,也可能来自规则被利用。

常见手段:

  • 新用户补贴被批量注册领取。
  • 邀请奖励被刷。
  • 低价商品叠加满减套利。
  • 退款后优惠权益未回收。
  • 组合订单拆单后优惠分摊错误。

防控方式:

  • 用户、设备、支付工具、收货地址、IP、账号关系图谱。
  • 优惠领取和核销频率限制。
  • 低价订单和 0 元订单专项监控。
  • 退款后优惠回收规则。
  • 活动预算和毛利率水位告警。

营销系统不能只看转化率,也要看补贴效率和异常套利。

3.6.18.9 供应商与结算资损防控

B2B2C 和多供应商平台里,资损常发生在平台、用户、商家、供应商之间的边界。

3.6.18.9.1 供应商履约一致性

平台订单和供应商订单必须建立映射:

字段说明
order_id平台订单
supplier_order_id供应商订单
supplier_id供应商
confirm_status供应商确认状态
supplier_amount供应商结算金额
currency币种
confirm_time确认时间
raw_response_ref原始响应

如果用户已支付但供应商未确认,系统必须明确处理策略:

  • 自动重试确认。
  • 切换供应商。
  • 退款。
  • 客服介入。
  • 平台承担差价履约。

不能让订单无限停在“处理中”。

3.6.18.9.2 商家结算

商家结算至少要对齐:

用户实付
  - 平台优惠承担
  - 商家优惠承担
  - 佣金
  - 手续费
  - 退款
  - 售后扣减
  = 商家应结算金额

结算资损常见问题:

问题风险
优惠承担方错平台和商家分摊错误
退款未冲减结算商家多结
费率配置错误佣金少收或多收
结算周期错误提前或延后结算
跨币种汇率错误汇兑损失
订单状态口径错误未完成订单被结算

结算系统必须使用不可变结算批次和结算明细。已出账批次不能随意修改,应通过调账单修正。

3.6.18.9.3 调账

调账是必要能力,但也是高风险能力。

调账必须具备:

  • 调账原因。
  • 调账对象。
  • 调账金额。
  • 关联订单或结算单。
  • 审批人。
  • 操作人。
  • 前后余额。
  • 财务凭证。

调账不能直接修改原始订单金额或支付金额。正确方式是追加调账流水。

3.6.18.10 权限、配置与后台操作防控

后台是资损防控的重点区域。

3.6.18.10.1 高风险操作分级
风险等级操作控制
P0批量退款、批量改价、批量发券、结算调账双人审批、白名单、限额、审计
P1活动规则上线、支付渠道切换、供应商映射修改审批、灰度、回滚
P2单商品改价、单订单补偿、单用户补券权限校验、理由必填、审计
P3查询、导出、低风险配置权限和水印

高风险操作要做到“能不能做、谁能做、做多少、怎么回滚、谁审批、如何审计”都有系统约束。

3.6.18.10.2 配置变更防控

配置变更必须和代码发布一样治理。

要求:

  • 配置有 schema 校验。
  • 配置有版本号。
  • 配置支持灰度。
  • 配置支持一键回滚。
  • 高风险配置需要审批。
  • 配置变更事件写入监控。
  • 配置生效范围可见。

例如支付渠道权重、活动预算、费率、退款规则、库存策略,都不应允许无审批直接全量生效。

3.6.18.10.3 禁止直接改库成为流程

事故处理中偶尔可能需要 DBA 级别的紧急修复,但不能把直接 SQL 变成日常流程。

直接改库的风险:

  • 绕过状态机。
  • 绕过幂等。
  • 绕过 Outbox。
  • 绕过审计。
  • 无法触发下游同步。
  • 无法被对账系统正确解释。

正确方式是建设修复工具:通过领域服务执行修复,自动写状态日志、Outbox、审计和对账标记。

3.6.18.11 风险规则与拦截系统

资损防控需要规则系统,但规则系统不能变成没人懂的黑盒。

3.6.18.11.1 规则类型
规则类型示例
阈值规则单笔退款超过 5000 元需要审批
比例规则商品价格低于近 7 日均价 50% 拦截
频率规则同用户 10 分钟内退款超过 3 次
预算规则活动预算消耗超过 90% 自动降级
状态规则已发货订单不能自动全额退款
关系规则同设备批量领取新人券
对账规则支付成功但订单未推进超过 1 分钟

规则结果不一定只有通过和拒绝,还可以是:

  • 放行。
  • 二次确认。
  • 人工审批。
  • 冻结对象。
  • 降级权益。
  • 触发对账。
  • 触发告警。
3.6.18.11.2 规则执行点

规则要放在关键业务动作前:

动作规则
计价价格、优惠、预算、毛利率
创单库存、优惠、风控、金额
支付金额一致、订单状态、渠道风险
退款可退金额、售后状态、风险审批
发券用户资格、频率、预算
改价成本价、影响范围、审批
结算订单完成、退款冲减、费率
人工修复权限、审批、幂等、审计

越靠近源头拦截,损失越小。等到财务 T+1 对账才发现,通常已经很难无痛修复。

3.6.18.11.3 规则治理

规则系统本身也会造成事故。

治理要求:

  • 规则有 owner。
  • 规则有版本。
  • 规则有命中日志。
  • 规则支持灰度。
  • 规则支持回滚。
  • 规则变更有审批。
  • 规则效果可监控。
  • 规则误杀可申诉和恢复。

没有治理的规则系统,最后会变成谁也不敢改、谁也解释不了的风险源。

3.6.18.12 监控与告警

资损监控要比普通可用性监控更贴近业务结果。

3.6.18.12.1 核心指标
指标
计价0 元单数量、负毛利订单数、折扣率分布、价格异常拦截数
支付支付金额不一致、重复渠道流水、支付成功订单未推进
退款超额退款拦截数、重复退款尝试、人工退款金额
库存超卖次数、预占超时未释放、库存对账差异
营销预算消耗速度、券重复核销、异常领取量
结算长短款、调账金额、结算差异
供应商平台支付成功但供应商未确认、供应商金额差异
后台高风险操作次数、审批拒绝数、批量操作影响量
3.6.18.12.2 告警分级
等级场景响应
P0重复扣款、重复退款、大面积错价、超卖、金额不一致立即止血,事故群,冻结风险对象
P1活动预算异常消耗、供应商确认失败激增、结算差异升高10 分钟内响应
P2单类商品价格异常、补偿积压、DLQ 增长值班处理
P3审计报表、趋势异常工单跟进

资损告警必须带上影响金额、影响订单数、影响用户数、开始时间、建议止血动作。只告警“规则命中异常”没有行动价值。

3.6.18.12.3 风险看板

资损看板建议分为四屏:

  1. 实时风险屏:P0/P1 风险、影响金额、冻结对象、当前止血状态。
  2. 交易链路屏:计价、创单、支付、退款、履约各环节异常。
  3. 权益与库存屏:库存、券、积分、预算、活动风险。
  4. 运营与审计屏:高风险操作、审批、调账、人工修复。

看板第一屏必须让值班同学知道:现在是否正在亏钱,亏在哪,是否已经止血。

3.6.18.13 资损事故处理

资损事故处理顺序和普通可用性事故不同。普通事故优先恢复服务,资损事故优先止血和保护证据。

推荐顺序:

  1. 确认是否仍在产生损失。
  2. 冻结风险入口,例如活动、商品、退款、支付渠道、供应商。
  3. 保护现场,保存日志、配置版本、订单快照、渠道流水。
  4. 估算影响范围和金额。
  5. 分类处理用户、商家、供应商和财务影响。
  6. 执行修复或补偿。
  7. 复核账务和数据。
  8. 输出复盘和长期改进项。
3.6.18.13.1 常见止血动作
事故止血动作
错价下架商品、暂停活动、冻结未履约订单
重复退款暂停自动退款、冻结售后入口
优惠被薅暂停活动、冻结异常券、限制用户
超卖停售 SKU、冻结库存、启动客服预案
支付状态错乱暂停相关渠道、启动查单和对账
供应商确认失败暂停售卖或切供应商
商家结算错误暂停出账批次、冻结打款

止血动作要提前做成开关,并有权限和审计。事故中临时找人写 SQL 关活动,通常已经晚了。

3.6.18.13.2 影响范围评估

资损事故必须回答:

  • 影响开始和结束时间。
  • 影响订单数。
  • 影响用户数。
  • 影响商家或供应商。
  • 最大可能损失金额。
  • 已实际发生损失金额。
  • 可追回金额。
  • 需要赔付金额。
  • 是否涉及合规和法务。

影响范围评估依赖快照、账本、审计和对账。如果平时没有这些数据,事故当天无法凭空生成。

3.6.18.14 典型案例

3.6.18.14.1 错价 0 元购

故障过程:

运营配置满 100 减 100
  -> 规则未限制适用品类
  -> 低价商品叠加新人券
  -> 大量订单应付金额变成 0
  -> 支付绕过
  -> 履约开始发货

防控点:

  • 计价校验应付金额不能低于最低支付金额,除非活动明确允许。
  • 活动规则必须绑定适用品类、商品、用户和预算。
  • 0 元单数量异常告警。
  • 负毛利订单实时拦截。
  • 履约前二次校验订单风险状态。
3.6.18.14.2 重复退款

故障过程:

用户申请退款
  -> 售后系统创建退款单
  -> 支付渠道退款超时
  -> 补偿任务重试
  -> 客服又手工发起退款
  -> 两条路径使用不同幂等键
  -> 用户收到两笔退款

防控点:

  • 退款以支付单累计可退金额为最终约束。
  • 售后单、退款单、支付单建立唯一关系。
  • 所有退款入口共用统一退款服务。
  • 人工退款也走同一幂等和审批流程。
  • 渠道超时先查单,不盲目再次退款。
3.6.18.14.3 库存重复释放

故障过程:

订单超时取消
  -> 关单任务释放库存
  -> 支付失败回调也释放库存
  -> 补偿任务再次释放库存
  -> 可售库存被加回三次
  -> 后续超卖

防控点:

  • 库存释放必须基于 reservation 状态机。
  • reservation_id + release_type 或 release ledger 唯一。
  • 库存余额不能由多个系统直接修改。
  • 库存对账发现 available + reserved + sold 不平立即冻结。

3.6.18.15 上线前资损检查清单

检查项必须回答
金额口径订单、支付、退款、结算金额是否有统一定义
快照订单是否保存商品、价格、优惠、税费和履约快照
幂等支付、退款、发券、扣库存是否有服务端幂等
状态机是否限制非法状态迁移和晚到事件
账本资金、库存、券、积分、预算是否有流水
预算活动和补贴是否有实时预算控制
可退金额是否防止累计退款超过实付
可售库存是否防止重复释放和超卖
人工操作高风险后台操作是否有审批、限额、审计
配置变更活动、费率、渠道、库存策略是否支持灰度和回滚
对账支付、库存、营销、结算、供应商是否有对账
冻结发现异常时能否冻结商品、订单、退款、活动、结算
监控是否有影响金额、订单数和异常趋势告警
Runbook值班同学是否知道如何止血和评估影响

3.6.18.16 小结:资损防控最终比拼的是系统约束力

资损防控是业务系统工程能力的综合考试。它同时考验领域建模、金额口径、状态机、幂等、账本、审批、对账、补偿、监控和事故处理。

对电商系统来说,资损防控不能只放在支付系统里,也不能只依赖财务 T+1 对账。真正可靠的做法是:

  1. 在计价阶段保证金额可解释。
  2. 在订单阶段固化交易快照。
  3. 在库存和营销阶段用账本保护权益变化。
  4. 在支付和退款阶段用幂等、状态机和渠道对账保护资金事实。
  5. 在供应商和结算阶段明确外部事实和结算口径。
  6. 在后台和配置中心治理人工高风险操作。
  7. 在事故中优先止血、冻结、保留证据和评估影响。

资损防控的最高标准不是“出了问题能赔”,而是系统能在大多数错误发生前拦住,在少数错误发生后快速发现,并在修复过程中保留完整证据链。

3.6.18.17 延伸思考:为什么资损防控最后会变成组织成熟度问题

资损防控表面上是技术能力,底层其实是在考验一整个组织是否真正接受了“高风险业务必须被系统约束”这件事。一个团队如果仍然习惯凭经验改价、靠口头约定补券、用 SQL 修订单、出了问题再让财务和客服兜底,那么再多的规则引擎和告警平台都只是表面繁荣。

真正有效的资损防控,最终会落实到几条朴素但难坚持的原则上:金额必须可解释,状态必须可追溯,人工操作必须可审批,修复动作必须可审计,异常趋势必须可止血,技术债务必须被持续偿还。做到这些,系统才不仅仅是“能交易”,而是“值得信任”。

3.7 组织文化篇:不惩罚文化与不可逾越的红线

系统治理走到最后,拼的从来不只是架构图和工具链,而是团队面对复杂性时的判断、纪律和克制。技术问题到最后往往会回到组织问题:需求压力如何与稳定性约束平衡,事故之后如何不甩锅但又不放松红线,谁来为长期治理负责,谁来捍卫对生产环境的敬畏。

3.7.1 复盘文化:从事故里赚回经验

事故复盘不是追责会。复盘的目标是让系统和团队变得更可靠。

一份高质量复盘至少包含:

  • 事故摘要。
  • 影响范围。
  • 时间线。
  • 根因。
  • 为什么监控没有更早发现。
  • 为什么保护机制没有阻止扩散。
  • 为什么恢复耗时这么长。
  • 已完成修复。
  • 后续行动项。
  • 行动项 owner 和截止时间。

复盘中最有价值的问题通常不是“谁犯了错”,而是:

  • 为什么这个错误能上线?
  • 为什么灰度没有发现?
  • 为什么告警没有及时响?
  • 为什么回滚不够快?
  • 为什么影响范围没有被隔离?
  • 为什么同类问题以前没有被系统性治理?

复盘行动项要避免空话。比如“加强测试”“提升稳定性意识”不是有效行动项。有效行动项应该是:

  • 为订单创建接口新增创单成功率 SLO 告警。
  • 将营销依赖从订单线程池拆到独立 bulkhead。
  • 支付回调增加渠道流水唯一约束。
  • 配置中心对支付渠道权重开启双人审批。
  • 每月演练一次库存服务超时降级。

3.8 本章小结:生产系统不是被设计稳定的,而是被治理稳定的

生产系统的长期稳定,从来不是靠某一个高可用组件、某一套报警平台,或者某几位经验丰富的值班同学单独撑起来的。它真正依赖的是一整套动态平衡机制:在业务增长时控制技术债,在事故发生时快速止血,在恢复之后把经验沉淀成治理规则,在组织层面守住对生产环境的敬畏。

如果只做保障,不做治理,团队会不断重复同一类事故;如果只谈治理,不建设恢复能力,系统会在第一次真实冲击中暴露脆弱;如果长期回避技术债,任何表面上的稳定都会在未来某个节点以更高利息偿还。

所以,本章真正想强调的不是某一项技巧,而是一种面向长期演进的工程观:治理负责治本,保障负责治标,技术债务决定系统的隐形成本,而这三者必须在真实生产运行中不断被重新校准。一个能够活二十年的系统,不是因为它从不出错,而是因为它能在每一次出错后变得更可控、更可解释,也更值得继续承载业务。

3.9 延伸思考:当系统继续增长,下一阶段的治理挑战是什么

走到这里,可以回头问三个更难、也更长期的问题:

  1. 当系统规模继续扩大时,我们是在增加能力,还是在增加复杂度?
  2. 当业务继续追求速度时,哪些技术债仍然可以接受,哪些已经越过了红线?
  3. 当平台和工具越来越多时,团队是否真的形成了自驱治理能力,还是仍然靠少数“救火队长”维持运转?

很多系统的分水岭,不在于某一次大促有没有扛住,而在于团队能不能从“出了问题会处理”,走向“复杂性上升时仍然知道该如何治理”。