24.8 最大努力通知
一句话
最大努力通知是 “尽力而为 + 对端幂等 + 主动查询兜底”:它不保证消息一定送达,只保证 “不会静默丢失”——因为有一条查询/对账路径能把漏掉的补回来。
| 通知体里带全量业务数据 | 数据泄露 + 体积膨胀,失败重传更贵 | | 不同事件共用幂等键 | 后到的事件被误判为重复 |
三种柔性方案的分工
| 方案 | 投递保证 | 谁兜底 | 适用 |
|---|---|---|---|
| 最大努力通知 | 有限次重试,不保证 | 对端主动查询 | 外部合作方、支付回调、低频通知 |
| 本地消息表 | 至少一次(同库事务保证) | 发送方重试 | 内部服务、强业务关联 |
| 事务消息 | 至少一次(半消息 + 回查) | 消息中间件 | 内部异步解耦 |
四个必备要素
| 要素 | 做法 | 不做会怎样 |
|---|---|---|
| 重试曲线 | 指数退避(1s/5s/30s/5m…),有上限 | 无脑重试打爆对端 |
| 幂等键 | 业务单号 + 事件类型,对端去重 | 重复通知导致重复入账 |
| 查询接口 | 对端可 GET /status?bizId= 补状态 | 丢一条就永久不一致 |
| 对账任务 | 定时比对双方状态,补差异 | 静默丢失无法发现 |
典型时序
本地事务提交 → 写“待通知”记录 → 异步通知(带 bizId、签名、时间戳)
├─ 成功:标记已通知
└─ 失败:退避重试(N 次后停止)
对端若未收到:定时任务按 bizId 查询我方状态,或我方定时对账补推
代码:通知 + 查询 + 对账三件套
// 1) 通知(带签名与幂等键;失败退避,超过上限转“待对账”)
for _, d := range backoff(1*time.Second, 5) {
if err := post(url, body, sign(body)); err == nil { return nil }
time.Sleep(d)
}
markPendingReconcile(bizID)
// 2) 查询接口(对端主动拉,补丢失的通知)
// GET /v1/orders/{bizId}/status → {status, updatedAt, sign}
// 3) 对账(每日/每小时):比对双方终态与金额
for _, diff := range diffTerminalState(ours, theirs) {
alert(diff); scheduleRepair(diff)
}
反模式
| 反模式 | 后果 |
|---|---|
| 把 “最大努力” 当 “一定送达” | 丢消息无人发现,账不平 |
| 没有幂等键 | 重试导致重复处理 |
| 没有查询接口 | 丢一条只能人工查 |
| 无限重试 | 对端故障时把自己也拖垮 |
| 不签名/不校验 | 伪造通知可篡改状态 |
三条铁律
🔑 不保证送达,但必须保证可发现;对账是它的另一半。
🔑 对端必须幂等;重试是设计的一部分,不是异常。
🔑 通知要签名 + 时间戳,防伪造与重放。
自检
Q1 为什么最大努力通知必须配 “查询接口”?
▸ 答案:因为它的重试有上限,必然存在最终没送达的情况。查询接口让对端能主动拉取状态,把 “丢的一条” 补回来;没有它,丢失就是永久的、且不可发现。
Q2 最大努力通知与本地消息表的本质区别?
▸ 答案:可靠性来源不同。本地消息表把 “发消息” 变成 “同库写一行”,靠本地事务保证 “业务成功则消息一定在”;最大努力通知不保证消息存在,靠对端查询/对账兜底。前者适合内部强关联,后者适合跨组织、弱依赖的通知。
Q3 重试曲线怎么设计?
▸ 答案:指数退避 + 上限 + 抖动:例如 1s、5s、30s、2m、10m(共 5 次),加 ±20% 随机抖动避免同步重试。超过上限后进入 “待对账” 状态,而不是继续无限重试。
Q4 对端为什么必须幂等,而不是 “我方保证不重发”?
▸ 答案:因为网络的不确定性让 “恰好一次” 无法保证:我方收到超时但对方已处理,重试就会重复。发送方无法知道自己是否 “已经送达”,所以幂等责任必须落在接收方(见 25.4 幂等设计与去重)。
Q5 对账任务应该比对什么?
▸ 答案:比对 “状态机终态 + 金额/数量”,而不是逐条消息:按业务单号拉双方状态,找出 “我方成功、对方未知/失败” 与 “金额不一致” 的记录。频率通常按业务容忍度(分钟级–日级),并输出差异报表供人工处理。
Q7 为什么通知要签名,而查询接口可以只靠 HTTPS?
▸ 答案:因为通知是服务端主动推给外部地址,对端无法确认 “这真是你发的”(可能被伪造/重放),所以要签名 + 时间戳 + 幂等键;查询是对端主动发起、走标准鉴权(token/mTLS),方向不同,威胁模型也不同。
Q6 最大努力通知的 “重试上限” 该设多少?
▸ 答案:按 “对端可恢复时间” 定:瞬时故障(重启/网络抖动)通常几分钟内恢复,5 次指数退避(1s/5s/30s/2m/10m)已覆盖约 15 分钟;超过这个窗口仍未成功,说明需要人工/对账,而不是继续重试。上限太长会拖住重试队列,太短则把可恢复故障留给了对账。
Q7 对端没有查询接口,只有通知,怎么办?
▸ 答案:建 “我方可查” 的兜底:提供一个只读查询接口给对端轮询(带鉴权与限流),或定期推送 “全量对账单”(文件/批量接口)。核心是必须有一条不依赖实时通知的路径,否则丢一条就是永久不一致。
关联
- 前置:24.6 本地消息表、24.7 事务消息 (半消息 / 回查)
- 并列:24.5 Saga (编排 / 协同 / 补偿)
- 下游:25.4 幂等设计与去重、67.3 对账与差异处理(待补)