支付回调超时处理,重试队列,回调失败存入队列定时重试
支付回调超时是支付对接中很常见的故障:用户已经付款,但平台的回调请求因为网络波动、应用卡顿或接口异常没有及时到达你的服务器。
如果直接丢弃这次回调,订单就会一直处于“未支付”状态,等到对账时才发现问题。
正确的处理思路是:先把回调数据持久化到一个重试队列,再通过定时任务轮询发送,直到对方返回成功或超过最大重试次数。
下面按实际落地步骤展开。
设计重试队列前需要准备什么
实现这套方案不需要额外购买中间件,用 Redis 或数据库就能完成。
建议提前准备好:
- 一个可用的 Redis 服务(版本 3.0 以上),用于存放待重试的回调数据;
- 自己业务环境的队列客户端,比如 Python 的
redis-py、PHP 的predis,或 Laravel 自带的队列驱动; - 一个用来模拟回调的测试接口,方便验证重试效果;
- 考虑好回调超时上限,通常支付平台允许的同步响应时间为 10~30 秒,具体以平台文档为准。
如果项目还没有接入消息队列,直接用 Redis 的 List 结构最省事,既可以当队列,也能配合定时任务实现延迟重试。
第一步:把回调失败的数据写入队列
在支付回调接口中,先捕获所有异常和超时情况,不能只依赖框架的异常处理。
伪代码如下:
import redis
import json
r = redis.Redis.from_url("redis://localhost:6379/0")
def pay_callback(request):
try:
# 原有业务处理:验签、更新订单状态、通知发货等
process_payment(request)
return {"code": "SUCCESS"}
except Exception as e:
# 回调失败,原始数据保存到队列,等待重试
payload = {
"order_no": request.get("order_no"),
"raw_data": request,
"retry_count": 0,
"last_error": str(e)
}
r.rpush("pay:callback:retry", json.dumps(payload))
return {"code": "FAIL"}
这里的关键点是:即使接口返回失败,支付平台短时间内可能会重推,但你不能依赖平台的推送次数。
把数据写进自己的 Redis 队列后,就掌握了重试的主动权。
第二步:定时任务扫描并重新发送回调
需要一个独立定时任务,建议每分钟跑一次,从队列左侧取出数据,重新请求自己的业务处理逻辑,而不是再次调用回调接口本身。
以 Python 的 schedule 库为例:
import schedule
import time
import json
def process_retry():
# 从队列取出待重试数据
item_json = r.lpop("pay:callback:retry")
if not item_json:
return
item = json.loads(item_json)
if item["retry_count"] >= 5:
# 超过最大次数,转入死信队列或记录告警
r.rpush("pay:callback:dead", item_json)
return
try:
# 再次执行业务处理
process_payment(item["raw_data"])
except Exception as e:
item["retry_count"] += 1
item["last_error"] = str(e)
# 重试次数没到,重新放回队列,也可以使用延迟队列控制频率
r.rpush("pay:callback:retry", json.dumps(item))
schedule.every(1).minutes.do(process_retry)
while True:
schedule.run_pending()
time.sleep(1)
定时任务需要保证只有一个实例在运行,否则多个进程同时 lpop 可能产生重复处理。
生产环境建议用 blpop 阻塞读取,并配合分布式锁或单实例部署避免并发消费。
第三步:设置重试次数上限和降级告警
重试不能无限进行。
上面代码中设置了 retry_count >= 5 后转入 pay:callback:dead 队列,这个队列要额外监控。
当死信队列有数据时,说明存在无法自动修复的订单,需要人工介入。
建议在死信写入时发送告警通知(企业微信/钉钉机器人),并记录完整日志,方便排查。
关于重试间隔,如果同一订单连续失败,最好使用指数退避,比如第一次等 1 分钟,第二次等 5 分钟,而不是每单都固定间隔。
Redis 的 ZSET 或消息队列的延迟消息可以实现,这里用 List 实现的是定时间隔,适合初期快速落地。
验证重试是否生效
写一个简单的测试脚本:
redis-cli lpush pay:callback:retry '{"order_no":"123","raw_data":{},"retry_count":0}'
然后手动停止业务处理服务,观察定时任务日志,确认数据被取出、重试次数递增,服务恢复后再次处理成功。
还可以故意把 retry_count 设为 4,触发最后一次重试,检查是否转入死信队列。
避坑时请记住:回调处理逻辑必须支持幂等,同一个订单状态重复更新多次不能出错;
队列消费失败要用 try/catch 包裹,避免异常导致进程退出;
Redis 数据要定期持久化,防止重启丢失。
如果对账后发现漏单,优先查死信队列和业务日志。
回调超时重试的核心就三件事:失败先入库、定时再发送、超限转人工。
按这个流程实现后,支付状态不一致的问题基本能被自动修复,剩下的只在死信队列里等待人工处理。
如果你在集成时遇到重试后依然失败,先检查验签函数是否依赖时间戳,以及回调地址是否被防火墙拦截。