Taocarts 知识

📅 2026-01-24 系统功能介绍

1688 API突然全部报错:限流击穿后的订单异步化改造

某个大促日,1688采购接口突然大面积返回 429 Too Many Requests。紧接着,订单同步停了两小时,十几个“采购中”的订单状态悬停,客户在群里催单,客服手动查单补单,漏掉三笔。事后复盘:调用太频繁,触发了应用级限流。根本原因在于订单创建后同步调用1688下单接口,没有做流量控制和异步削峰。

一、限流不是加个sleep就完事

直接对1688接口调用做限流,常见做法是在调用前 usleepthrottle。但同步阻塞式调用下,等待时间会堆积在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限流太严,而是系统设计时假设“第三方接口永远可用且足够快”。把同步调用改成令牌桶+队列之后,采购链路才真正具备抗冲击能力。下一篇聊聊仓库侧的包裹入库和合包策略——队列在那里同样扮演关键角色。

wechat wechat qr