独立站Webhook可靠性:重试机制、幂等性、签名验证与监控

Webhook 不是"发了就完事",而是一个带确认的投递协议——对方没在 3 秒内回 2xx,你就必须重发。做可靠 Webhook 要同时守住四件事:HM...

Webhook 可靠性四要素:收得到、认得准、不重复、看得见Webhook reliability: receive, verify, dedupe, observe 反直觉:Webhook 不是”发了就行”——对方不回 2xx,你就必须重发Counter-intuitive: a webhook is not sent until a 2xx comes back 要素Element 做法How 量化标准Standard 签名验证Signature HMAC-SHA256 校验头HMAC-SHA256 header check 时间戳容差 5 分钟5-min timestamp skew 快速返回Fast ack 先回 2xx 再异步处理Ack 2xx, process async 3 秒内必须响应Respond within 3s 幂等去重Idempotent 事件 ID 落表去重Dedupe by event id 同一事件重试 N 次只处理 1 次Retry N times, process once 重试退避Retry backoff 指数退避 + 抖动Exponential + jitter 最多 5 至 8 次,约 24 小时5-8 retries over ~24h 核心原则:先验证签名,再快速回 2xx,事件入库后异步处理Verify signature, ack fast, then process async 不要在同步请求里做耗时业务,否则对方判定超时开始重发,造成重复Never do slow work inline or retries will duplicate work 监控红线:重试队列堆积超过 100 条或连续 30 分钟无成功投递,立即告警Alert: backlog > 100 or zero success for 30 min 超过重试上限的事件进死信队列,人工补单,不能静默丢弃Dead-letter failed events; never silently drop
Webhook 可靠性四要素:签名验证、快速确认、幂等去重、重试退避与死信监控

独立站Webhook可靠性:重试机制、幂等性、签名验证与监控

结论先行:Webhook 不是”发了就完事”,而是一个带确认的投递协议——对方没在 3 秒内回 2xx,你就必须重发。做可靠 Webhook 要同时守住四件事:HMAC 签名验证防伪造、先快速回 2xx 再异步处理、按事件 ID 幂等去重、指数退避重试配死信队列。最常见的事故不是”没收到”,而是”收到了却处理了两次”,或者”处理太慢触发重发,结果同一笔订单进了三单”。

一、Webhook 为什么天然不可靠

Webhook 是对方系统主动把事件 POST 给你的回调地址。它建立在 HTTP 之上,而 HTTP 本身是无连接、不保证送达的:网络抖一下、你的服务重启一下、处理超时一下,这次投递就可能丢。更麻烦的是”不知道对方收没收到”——你发出去了,但没收到 2xx 响应,到底是网络丢了还是对方处理了但响应丢了?

反直觉的判断在这里:Webhook 系统默认就会重试,所以你的接收端必须按”会被重复投递”来设计,而不是假设它只会来一次。Stripe、Shopify 这类成熟平台的标准做法是:接收端不返回 2xx 就按指数退避重发,最多发 5 至 8 次、跨约 24 小时。

你如果没做幂等,同一笔支付事件就会被入账好几次。线索到系统的打通,见官网线索到 CRM,那里同样要面对重复事件。

四件事缺一件,可靠性就掉一档。下面这张表把每要素”做什么、漏了会怎样”列清。

要素 作用 标准做法 漏了的后果
签名验证 确认请求真来自对方 HMAC-SHA256 校验头 被伪造事件注入假订单
快速确认 避免对方超时重发 先回 2xx,再异步处理 处理慢导致重复投递
幂等去重 重复事件只处理一次 事件 ID 落表去重 同一订单入账多次
重试退避 临时失败最终送达 指数退避加抖动 偶发故障直接丢单
死信监控 失败事件不静默丢 超限进死信队列告警 长期丢单无人知

二、签名验证:确认这个请求真不是别人伪造的

Webhook 回调地址是公网可访问的 URL,任何人都能往这个地址 POST 数据。如果不验签,攻击者直接构造一条”支付成功”的假事件,你后台就可能真的给他开通权限或发订单。这不是理论风险,是真实被打过的攻击面。

标准做法是 HMAC-SHA256:对方用约定的密钥对请求体做签名,放在请求头里(如 X-Signature);你收到后用同一个密钥对原始请求体再算一次,两边比对,一致才处理。

两个细节必须做:第一,用定时安全比较函数比对,不要用普通字符串 ==,防时序攻击;第二,校验请求头里的时间戳,超过 5 分钟容差直接拒绝,防止有人录下旧请求重放。密钥泄露后要能立即轮换。

校验项 做法 不通过的处理
签名是否匹配 HMAC-SHA256 重算比对 直接 401 拒绝
时间戳是否新鲜 与服务器时间差 ≤ 5 分钟 拒绝,防重放
密钥是否过期 支持新旧密钥并存轮换 用旧密钥再试一次
请求体是否被改 用原始字节算签名 不要先 JSON 解码再签

三、快速确认与异步处理:别在请求里干重活

这是最容易踩的坑。很多人收到 Webhook 后,在同步请求里做一连串重活:查库、发邮件、调第三方接口、写日志,一套下来花了 5 秒。对方等 3 秒没收到 2xx,判定超时,开始重发——你这边其实还在处理第一条,第二条又进来了。

正确模式是三段式:收到请求后,先验签(毫秒级),验过立刻把事件原样落库并返回 2xx,然后后台异步任务慢慢处理。这样对方 3 秒内一定拿到确认,不会重发;重活慢一点没关系,反正用户已经收到回执了。处理失败的事件留在队列里,由异步任务自己重试。

场景示例:支付成功事件进来。接收端只做三件事:验签、把事件 ID 和原始 payload 写一张 events 表、立刻返回 200。真正的”加库存、发邮件、通知 ERP”由一个后台 worker 从 events 表里取出待处理事件慢慢做。哪怕发邮件要 10 秒,也不会影响对方重发。表单送达的可靠性思路,可对照询盘表单可靠性。

阶段 动作 耗时目标 失败处理
同步接收 验签 + 落库 + 回 2xx 小于 300 毫秒 验签失败直接拒绝
异步处理 查库、发通知、调下游 秒级到分钟级均可 失败留在队列重试
重试 按退避间隔重跑 指数退避 超上限进死信队列
人工补单 查死信队列手动触发 每日巡检 记录补单原因

四、幂等性与重试:同一事件来十次也只处理一次

幂等的意思是:同一个操作执行一次和执行十次,结果完全一样。Webhook 必须天然幂等,因为你无法阻止对方重发。实现方式很简单:对方每个事件都带一个全局唯一的事件 ID,你处理前先查这张 events 表,见过这个 ID 就直接返回成功、跳过处理。

重试策略用指数退避加抖动:第一次失败等 1 分钟,第二次 2 分钟,第三次 4 分钟,以此翻倍,最多 5 至 8 次、总跨度约 24 小时。加随机抖动是为了避免所有失败事件在同一秒集中重试,把你的服务再压垮一次(这叫重试风暴)。超过重试上限的事件不丢,进死信队列,等人介入。

重试次数 退避间隔(约) 累计时间 说明
第 1 次 1 分钟 1 分钟 临时网络抖动
第 2 次 2 分钟 3 分钟 短暂服务重启
第 3 次 4 分钟 7 分钟 下游临时不可用
第 4 次 8 分钟 15 分钟 较长故障
第 5 至 8 次 15 至 30 分钟 约 24 小时 仍失败进死信队列

五、监控与死信:别让丢单静默发生

Webhook 最阴险的故障是”静默丢单”:对方觉得发成功了,你这边一条记录都没有,几周后客户投诉”我付了钱为什么没发货”,你才发现事件根本没到。所以监控必须盯三件事:投递成功率、重试队列堆积、死信队列数量。

阈值给具体数字:重试队列堆积超过 100 条,或连续 30 分钟没有任何成功投递,立即告警到你常用的渠道。死信队列每天巡检一次,有积压就人工补单。投递成功率低于 99% 就要查——健康状态下 Webhook 成功率应该接近 99.9%,掉到 99% 以下说明有持续性问题。通用监控告警怎么搭,见站点监控与告警。

监控指标 健康值 告警阈值 动作
投递成功率 ≥ 99.9% 连续 1 小时 < 99% 查接收端日志
重试队列堆积 接近 0 超过 100 条 查下游是否卡住
死信队列 0 出现任意积压 人工补单并定位
连续无成功投递 持续有事件 30 分钟为 0 查回调地址是否被封
验签失败次数 接近 0 突增 10 倍 可能有人在扫接口

FAQ 常见问题

为什么我的 Webhook 有时候会收到重复的订单事件?
因为对方在你没及时返回 2xx 时会自动重发。成熟平台默认最多重发 5 至 8 次、跨约 24 小时。解决办法不是让对方别重发,而是在你这边按事件 ID 做幂等去重:收到事件先查这张表,处理过就直接返回成功、跳过业务。重复投递是 Webhook 的常态,必须按会重复来设计。
Webhook 收到后应该立刻处理还是先返回?
先返回。验签通过后立刻把事件落库并在 3 秒内返回 2xx,真正的业务处理放到后台异步做。如果你在同步请求里发邮件、查库、调下游花了 5 秒,对方会判定超时开始重发,反而造成重复。异步 worker 慢慢处理,慢一点没关系。
不做签名验证行不行,反正是走 HTTPS?
不行。HTTPS 只保证路上不被窃听,你的回调 URL 是公网可访问的,任何人都能直接往这个地址 POST 数据。不验签,攻击者可以自己构造一条支付成功的假事件,骗你后台发货或开通权限。必须用 HMAC-SHA256 校验请求头签名,并加 5 分钟时间戳容差防重放。
重试间隔怎么定,为什么不能固定每分钟重试?
用指数退避加随机抖动:1 分钟、2 分钟、4 分钟、8 分钟,最多 5 至 8 次跨约 24 小时。固定间隔每分钟重试的问题是,一旦下游故障,所有失败事件会在同一秒集中重试,把刚恢复的服务再压垮,这叫重试风暴。加随机抖动打散这个时间点。
重试很多次还是失败的事件怎么处理?
不能静默丢弃,要进死信队列。超过重试上限的事件单独存一张表,每天巡检一次,人工确认后手动触发补单,并记录失败原因。静默丢弃是最危险的做法——几周后客户投诉没发货,你在日志里根本查不到这笔事件曾经来过。
怎么知道 Webhook 有没有在悄悄丢单?
盯三个数:投递成功率低于 99% 就查,重试队列堆积超过 100 条告警,连续 30 分钟没有任何成功投递立即告警。健康状态下投递成功率应接近 99.9%。再加一条:每天检查死信队列是否为 0。这几个告警接好,静默丢单就能在当天被发现,而不是几周后被客户发现。

写在最后

Webhook 可靠性的核心认知是:它不是单向推送,而是一个”不确认就重发”的双向协议。你设计接收端时,必须把”会被重复投递、会被伪造、会丢”当成默认前提,而不是小概率事件。四个要素里,幂等去重和快速确认是最容易被忽略、出事最多的两个,先把这两个做对,事故率就掉一大半。

落地三步:第一,接收端只做验签、落库、3 秒内回 2xx,业务全部异步;第二,按事件 ID 建去重表,重试用指数退避加抖动;第三,死信队列每天巡检,投递成功率和堆积量接告警。做完这三步,你的回调链路就从”偶尔丢单靠运气”变成”丢了当天能发现、能补回来”。

一句可以记住的话:Webhook 的可靠不靠对方发得稳,靠你这边收得笨——验签、回得快、去得重,宁可重复处理,也别漏一单。

环形山
环形山 环形山专家

机构官方 · 环形山 Crater

环形山(Crater)是一家专注 WordPress 独立站开发 + Google SEO + GEO 全域增长的出海服务公司,服务智能硬件、工业装备、SaaS、新能源等赛道。

产品能力方法论白皮书行业报告
965篇分享
78篇知识库