上个月帮朋友接了个物流轨迹同步的活:每天凌晨把前一天产生的两三千个单号批量推给上游查询接口。上线头三天还好,第四天运维半夜打电话说上游投诉我们同一个单号一天推了七八次,那边订单状态被反复覆盖,客户都来问了。
一开始我以为是网络重试没控好,翻了日志才发现没那么简单——超时的、返回 500 的、上游直接断连的,三种错误走了三条不同的重试路径,最后在并发下互相踩。前后花了两个晚上把重试、幂等和死信这套东西理清楚,记录下来。
批量任务是这样跑的:读本地待推列表 → 开 20 个线程调上游接口 → 成功的标记 done,失败的记下原因。问题出在失败重试这一段:
三个原因叠在一起,一个单号被推四五次很正常。
最关键的一条:每个单号每次推送带一个唯一的 requestId,上游用它去重。requestId 不能随机生成,要和"单号+推送批次"绑定,这样重试时同一个业务请求重试多少次都是同一个 id:
// requestId = MD5(trackingNo + batchDate + pushScene)
// 例:MD5("SF1234567890" + "2026-10-12" + "track_sync")上游收到后先查这个 requestId 有没有处理过,处理过直接返回上次的结果,不再执行业务。这一步做完,即使我们重推十次,上游侧也只生效一次。
一开始重试是固定 5 秒一次,连续重试 5 次。上游抖动时,20 个线程同时每 5 秒打一轮,等于雪上加霜。改成指数退避:
// 第 n 次重试等待 = base * 2^(n-1),base=1秒
// 第1次:1s 第2次:2s 第3次:4s 第4次:8s 第5次:16s
// 再加随机抖动正负20%,避免多个线程同时醒来
delay = base * pow(2, retryCount) * (0.8 + random*0.4)这样失败后不是立刻猛打,而是把间隔拉长,给上游恢复时间。总重试次数控制在 5 次,超过就进死信队列人工看。
5 次还失败的单号,别留在主任务里反复重试拖垮整批,扔到死信表:
dead_letter 表字段:
tracking_no, request_id, last_error, fail_count, next_retry_time, create_time并发捞取那个坑也要堵:别用"select * where status=待重试"然后多线程抢,改成数据库行锁或者分布式锁:
// 用 for update 锁住要处理的行,处理完再提交
select * from push_task where status='pending' order by id limit 50 for update skip locked;skip locked 让别的线程直接跳过已经被锁的行,不会两个线程捞到同一个单号。
错误类型 | 处理策略 | 最大重试 |
|---|---|---|
连接超时/断连 | 指数退避重试 | 5 次 |
上游返回 500 | 指数退避重试 | 3 次 |
上游返回 4xx 业务错 | 不重试,直接死信 | 0 次 |
上游返回 200 但业务失败 | 查日志人工处理 | 0 次 |
这次物流轨迹批量同步是在固乔快递批量查询助手的任务调度上接的,我主要负责重写重试策略、补 requestId 幂等键和死信表那套逻辑,改完后上游那边重复推送的投诉当晚就没了,整批同步从原来的 40 分钟缩到 18 分钟。
以上是个人实践记录,各平台具体功能以官方实时信息为准。
你们做批量任务时,幂等键是放在请求头还是请求体里?上游那一侧一般怎么落库去重?
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。