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 对端没有查询接口,只有通知,怎么办?

▸ 答案:建 “我方可查” 的兜底:提供一个只读查询接口给对端轮询(带鉴权与限流),或定期推送 “全量对账单”(文件/批量接口)。核心是必须有一条不依赖实时通知的路径,否则丢一条就是永久不一致。

关联