1688 API突然全部报错:限流击穿后的订单异步化改造
某个大促日,1688采购接口突然大面积返回 429 Too Many Requests。紧接着,订单同步停了两小时,十几个“采购中”的订单状态悬停,客户在群里催单,客服手动查单补单,漏掉三笔。事后复盘:调用太频繁,触发了应用级限流。根本原因在于订单创建后同步调用1688下单接口,没有做流量控制和异步削峰。
一、限流不是加个sleep就完事
直接对1688接口调用做限流,常见做法是在调用前 usleep 或 throttle。但同步阻塞式调用下,等待时间会堆积在HTTP进程里,轻则超时,重则拖垮整个订单创建接口。
taocarts 在处理采购高峰期(日单接近企业认证账号的API日配额上限)时,采用的方案是:令牌桶限流 + 消息队列异步化,将“请求1688接口”这个动作从订单创建的同步链路中剥离。
令牌桶限流器(Redis + Lua):
-- rate.lua
-- KEYS[1] = token bucket key
-- ARGV[1] = rate (tokens per second)
-- ARGV[2] = capacity (max tokens)
-- ARGV[3] = requested tokens (usually 1)
-- ARGV[4] = current timestamp (ms)
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local requested = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
local bucket = redis.call('hmget', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1])
local last_refill = tonumber(bucket[2])
if tokens == nil then
tokens = capacity
last_refill = now
end
local refill = math.floor((now - last_refill) * rate / 1000)
if refill > 0 then
tokens = math.min(capacity, tokens + refill)
last_refill = now
end
if tokens >= requested then
tokens = tokens - requested
redis.call('hmset', key, 'tokens', tokens, 'last_refill', last_refill)
redis.call('expire', key, 10)
return 1
else
return 0
end
调用时,每个订单尝试获取1个令牌,成功则投递到队列,失败则返回 429 并由上层重试(客户端可稍后重试)。这个Lua脚本保证原子性,每秒最多放行 rate 个请求,最高积攒 capacity 个令牌应对短时突发。
二、队列削峰:不让限流变成订单丢失
令牌通过后,不直接调用1688 API,而是将采购任务推送到RabbitMQ(或Redis队列,量不大时够用)。独立消费者进程从队列拉取任务,消费时再次检查令牌(双重保障),然后调用1688下单接口。
队列结构:
// 投递采购任务
$task = [
'order_id' => $orderId,
'user_id' => $userId,
'items' => $items,
'retry_count' => 0,
'max_retry' => 3
];
$queue->push('purchase_orders', json_encode($task));
消费者(常驻Worker):
while (true) {
$task = $queue->pop('purchase_orders');
if (!$task) {
sleep(1);
continue;
}
// 再次尝试获取令牌(避免多个worker瞬间打满限额)
if (!$this->rateLimiter->allow('1688_api', 5, 10)) {
// 没有令牌,重新放回队列并延迟5秒
$queue->later('purchase_orders', 5, $task);
continue;
}
$result = $this->call1688Api($task);
if ($result['code'] == 429) {
// 被限流,增加重试计数,延迟后重新入队
$task['retry_count']++;
if ($task['retry_count'] <= $task['max_retry']) {
$queue->later('purchase_orders', pow(2, $task['retry_count']) * 5, $task);
} else {
$this->markOrderFailed($task['order_id'], 'API限流超重试次数');
}
} elseif ($result['code'] == 200) {
$this->updateOrderPurchaseStatus($task['order_id'], 'purchased', $result['data']);
} else {
// 其他错误,同样重试
$task['retry_count']++;
if ($task['retry_count'] <= $task['max_retry']) {
$queue->later('purchase_orders', 30, $task);
}
}
}
这里的关键trade-off:同步转异步后,订单创建接口的响应时间从原来等待1688 API的1-3秒降到10ms以内,用户体验提升明显。代价是采购状态不再实时反馈,需要前端轮询或WebSocket推送。对于代购场景,客户对“下单后几秒内看到采购中”并没有硬性要求,可接受异步。
三、限流维度的配合:应用级 + 用户级 + IP级
1688开放平台的限流是多维度的(应用级QPS、用户级QPS、IP级)。taocarts 在配置限流器时,针对三个维度分别设置令牌桶:
- 应用级(AppKey):所有订单共享,每秒放行30个(低于企业账号50 QPS的安全线)
- 用户级(每个1688采购账号):每秒放行3个
- IP级(出口IP):每秒放行40个
三个维度同时校验,任何一个不通过则任务延迟重试。这样做是为了避免某个维度的超限把其他正常请求拖垮。例如,某个供应商账号被1688临时降权,只会影响该账号的任务,其他账号仍然能正常采购。
四、异常兜底:死信队列与人工干预面板
即便做了限流和重试,仍有小概率事件导致任务最终失败(如1688接口连续24小时不可用)。taocarts 的死信队列机制:重试超过最大次数后,任务转入 failed_purchases 表,并触发告警(钉钉/邮件)。后台管理面板提供“重新采购”和“标记人工处理”按钮,运营人员可一键重试或手动补单。
日单300左右时,这套机制使因API限流导致的订单悬停比例从改版前的约2%降到0.2%以下。更重要的是,不再需要半夜爬起来手动重跑失败任务。
回过头看那次大促事故,问题根源不是1688限流太严,而是系统设计时假设“第三方接口永远可用且足够快”。把同步调用改成令牌桶+队列之后,采购链路才真正具备抗冲击能力。下一篇聊聊仓库侧的包裹入库和合包策略——队列在那里同样扮演关键角色。