本文目录导读:

Webhook 本身没有自动重试机制,但许多 Webhook 的发送方(如 GitHub、Stripe、企业微信等)会在事件投递失败时提供自动重试功能。
具体实现取决于谁在发送 Webhook以及后端如何接收,以下是几种常见情况及实现方法:
依赖 Webhook 提供方自带的自动重试机制
大多数主流 Webhook 服务商都内置了重试策略。
- 工作原理:当你的服务器返回
5xx(服务器错误)或超时(> 5秒)或返回特定的4xx时,发送方会稍后再次发送相同的 Webhook 请求。 - 典型策略:
- 指数退避:第一次失败后等1分钟,第二次等5分钟,第三次等30分钟……直到达到最大重试次数(如3-10次)。
- 固定间隔:例如每15分钟重试一次,共重试24小时。
- 常见例子:
- GitHub:返回非
2xx或超时则重试,共重试3次,间隔约30秒、2分钟、5分钟。 - Stripe:会重试最多3天,间隔逐渐增加。
- 支付宝/微信支付:通常有异步通知机制,会重试8-12次,间隔递增(例如2m, 10m, 10m, 1h, 2h, 6h, 15h)。
- GitHub:返回非
你只需要:
- 确保你的服务器端接收端点同步处理逻辑(收到请求 -> 处理成功 -> 立即返回
200 OK给发送方)。 - 如果处理失败(如数据库挂了),返回非
2xx状态码(如500或503),发送方就会自动重试。
手动实现或增强重试逻辑(当提供方不做重试时)
如果你使用的 Webhook 发送方没有内置重试,你需要自己动手,常见方案:
方案 A:使用消息队列 + 失败重试(推荐)
这是最稳健的做法,脚本不直接处理 Webhook,而是把事件存起来。
- 接收请求:脚本收到 Webhook 后,立即返回
200 OK。 - 写入队列:把事件数据写入一个可靠的队列(如 Redis List, RabbitMQ, AWS SQS)或数据库(如 MySQL)。
- 异步处理:另一个 Worker 脚本不断从队列中拉取事件进行处理。
- 重试逻辑:Worker 处理失败,它不会丢弃消息,而是:
- 将消息重新放入队列(延迟队列)。
- 更新数据库中的重试计数。
- 假设最大重试3次,指数退避(1分钟 -> 5分钟 -> 30分钟)。
- 超过最大次数,移入死信队列供人工排查。
伪代码示例(Python + Redis + 简单循环):
import redis, time, json
r = redis.Redis()
MAX_RETRIES = 3
def process_webhook(event_data):
# 真正的业务逻辑,例如给用户发邮件
# 如果失败,抛出异常 or return False
pass
def worker():
while True:
# 从阻塞队列取消息,起一个便于重试的名字
item = r.blpop("webhook_queue", timeout=0)
data = json.loads(item[1])
retries = data.get('retries', 0)
try:
process_webhook(data['payload'])
print("处理成功")
except Exception as e:
print(f"处理失败: {e}")
if retries < MAX_RETRIES:
data['retries'] = retries + 1
# 指数退避延迟:2^retries * 60 秒
delay = (2 ** retries) * 60
# 放入延迟队列(需要自己实现或使用Redis的ZSet)
# 简单实现:先睡一会再放回主队列(不推荐但能用)
time.sleep(delay)
r.lpush("webhook_queue", json.dumps(data))
print(f"放入重试队列, 第{retries+1}次")
else:
# 超过次数,记录到死信日志
r.lpush("webhook_dead_letter", json.dumps(data))
print("超过重试次数,记录死信")
方案 B:使用定时任务(Cron Job)轮询
如果你用 Spring Boot Scheduler 或 Linux Cron。
- 脚本不直接处理实时事件。
- 将事件存入数据库(
webhook_events表),状态为pending。 - 定时任务(如每5分钟执行一次)扫描
status = pending AND retries < 3的记录。 - 尝试处理,成功则更新状态为
completed;失败则retries + 1,并根据last_attempt_at判断是否已达到下次重试时间(指数退避)。
关键注意事项(避免事故)
| 问题 | 解决方案 |
|---|---|
| 重复事件 | 提供方重试、MQ重试都可能导致同一个事件被处理多次。你的脚本必须是幂等的(通过 event_id 去重,如果已处理则直接返回成功)。 |
| 超时 | 如果业务处理需要很长时间(如生成报告),不要直接在 Webhook 回调里做。先返回 202 Accepted,然后异步处理。 |
| 死信 | 所有重试方案都必须有最终兜底,当重试次数用完后,通知管理员或写入专门的文件/队列,避免数据丢失。 |
| 请求验证 | 重试会重复发送相同的请求,务必验证签名(如 HMAC-SHA256)或 IP 白名单,防止被伪造。 |
- 对于主流服务(GitHub, Stripe, 微信支付):你的脚本不需要写重试逻辑,只需要确保处理失败时返回错误码(
5xx),它们会自动重试。这是最省事的做法。 - 对于自建系统或无重试的发件方:你需要用消息队列+死信机制来手动实现指数退避重试,这是最可靠的架构,避免直接在脚本里写
while循环sleep。
你的场景是哪种发件方?如果是通用场景,建议把“返回错误码让提供方重试”作为第一选择。