http.handlerfunc不能直接处理异步回调超时与重试,因其易阻塞导致响应延迟、被重试引发重复处理;必须解耦接收与执行:校验签名后立即返回200,将payload写入持久化队列(如redis stream或asynq),由后台worker异步消费并实现幂等。

为什么 http.HandlerFunc 不能直接处理异步回调的超时与重试
异步通知(比如支付平台回调、Webhook)本质是第三方主动发起的 HTTP 请求,你无法控制它何时来、是否重发、是否超时。用标准 http.HandlerFunc 直接写逻辑,容易卡在耗时操作(如数据库写入、下游服务调用)上,导致响应延迟甚至超时被对方重试,最终造成重复处理。
真正要解决的不是“怎么接收请求”,而是“怎么确保只处理一次、失败可追溯、重试不雪崩”。关键在于把「接收」和「执行」解耦。
- 收到回调后,立即校验签名、解析参数,然后快速返回
200 OK - 将有效载荷(payload)写入消息队列或持久化存储(如 PostgreSQL 表、Redis Stream),再交由后台 worker 异步消费
- 避免在 handler 中做任何可能阻塞 >1s 的操作(包括未加
context.WithTimeout的 DB 查询)
用 github.com/hibiken/asynq 实现幂等回调消费
asynq 是 Go 生态中轻量但生产就绪的异步任务库,天然支持失败重试、延迟调度、任务去重,比手撸 goroutine + channel 更可靠。
核心做法:回调 handler 把请求体序列化为 asynq.Task,推入队列;worker 单独进程监听并执行业务逻辑,通过 task.ID 或自定义 key 做幂等判断。
// 回调入口
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-Signature")
if !verifySig(body, sig) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
// 快速响应
w.WriteHeader(http.StatusOK)
// 推入队列,使用 biz_id 做唯一任务 ID 防重
task := asynq.NewTask(
"webhook:pay_notify",
map[string]interface{}{"raw_body": string(body)},
asynq.TaskID(fmt.Sprintf("pay_%s", getBizID(body))),
)
_, _ = client.Enqueue(task, asynq.Queue("webhook"), asynq.Timeout(30*time.Second))
}
- 务必设置
asynq.TaskID,否则相同 payload 多次推送会生成多个任务 - 不要把敏感字段(如密钥)塞进 task payload,应只存 ID,worker 再查库获取详情
- worker 启动时注册 handler,需自行实现幂等:先查 DB 是否已处理该
biz_id,再执行后续逻辑
如何防止第三方重复推送导致的「双花」问题
微信/支付宝回调明确说明“可能多次推送”,常见错误是只依赖数据库 INSERT ... ON CONFLICT DO NOTHING,但没覆盖所有失败场景(例如事务中途 panic、写缓存成功但 DB 失败)。
安全做法是引入状态机 + 唯一约束:
- 数据库表必须有
biz_id UNIQUE约束(如订单号、通知 ID) - 状态字段至少包含:
pending(刚入库)、processing(开始执行)、succeeded/failed - worker 消费时,用
UPDATE ... SET status = 'processing' WHERE biz_id = ? AND status = 'pending'尝试抢占,仅当影响行数为 1 才继续执行 - 失败后更新为
failed并记录 error,便于人工干预
这样即使两个 worker 同时拿到同一任务,也只有一个能真正执行,另一个直接跳过。
本地开发调试时如何模拟真实异步回调行为
别用 curl 手动发请求反复测——你没法模拟网络抖动、重试间隔、签名失效等真实条件。
推荐组合方案:
- 用
ngrok http 8080暴露本地服务,让第三方平台把回调打到公网地址 - 自己写个简易重放脚本,读取历史回调 payload 文件,带不同时间戳和签名重发(注意重放攻击防护,测试环境可临时关闭签名校验)
- 在 handler 开头加日志:
log.Printf("webhook received, id=%s, ts=%s", getBizID(body), r.Header.Get("X-Timestamp")),方便对齐上下游时序
最易忽略的是时钟同步:如果服务器时间比第三方快/慢超过 5 分钟,很多平台(如支付宝)会直接拒签。调试前先 ntpdate -s time.windows.com 或检查容器内时间是否与宿主机一致。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











