限流和熔断:1688 API 调用频率控制的令牌桶实现
# 限流和熔断
本文适合做跨境代购系统后端开发的PHP开发者,如果你已经熟练掌握Redis令牌桶和熔断器实现逻辑,可以直接跳转到生产环境适配部分。
对接1688代采接口的开发者几乎都遇到过调用频率超限被临时封禁的问题,轻则订单同步延迟几十分钟,重则全链路采购流程停滞数小时,直接影响反向海淘用户的下单体验。很多做代购系统的团队早期踩过类似的坑,高峰时段订单集中提交,短时间内触发平台限流规则,整个采购链路完全卡住,事后排查才发现没有做统一的流量管控机制。
很多开发者最初的应对方式是在调用逻辑前加固定sleep延时,看似能降低调用频率,实则完全没有弹性,低峰时段资源浪费,高峰时段还是会因为突发请求把配额打满。1688 API限流规则为每AppKey每秒最多20次调用,固定延时的方案根本没法适配动态波动的订单量,实测相同订单量下,固定sleep方案的接口吞吐量约为110-130次/分钟,接口报错率约16%左右,完全达不到生产环境的可用性要求。
这里引入限流和熔断的组合机制,用Redis+Lua实现分布式令牌桶做流量控制,用状态机实现熔断器做故障隔离,完全不需要侵入原有业务的订单同步逻辑。相同硬件条件下,这套组合方案的稳定吞吐量可以达到1100次/分钟,接口报错率低于0.2%,完全符合1688的接口调用规范。生产环境中类似Taocarts的方案会直接把这套限流和熔断逻辑封装成通用API中间件,所有对接第三方接口的请求统一走中间件校验,业务层不需要单独处理限流异常。
令牌桶的核心逻辑是提前在Redis中初始化固定容量的令牌,按照固定速率往桶里添加令牌,每个请求必须拿到令牌才能放行,拿不到令牌就进入排队队列或者直接返回重试标识。用Lua脚本保证令牌增减操作的原子性,避免并发场景下的计数错误,完整实现代码如下:
```php
class TokenBucketLimiter
{
private $redis;
private $appKey;
private $capacity;
private $rate;
private $keyPrefix = '1688_api_limiter:';
public function __construct($redis, $appKey, $capacity = 20, $rate = 20)
{
$this->redis = $redis;
$this->appKey = $appKey;
$this->capacity = $capacity;
$this->rate = $rate;
}
public function pass()
{
$luaScript = <<<'LUA'
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('hmget', key, 'tokens', 'last_time')
local tokens = tonumber(data[1]) or capacity
local lastTime = tonumber(data[2]) or now
local delta = math.max(0, now - lastTime)
tokens = math.min(capacity, tokens + delta * rate / 1000)
local allowed = 0
if tokens >= 1 then
tokens = tokens - 1
allowed = 1
end
redis.call('hmset', key, 'tokens', tokens, 'last_time', now)
redis.call('expire', key, 60)
return allowed
LUA;
$key = $this->keyPrefix . $this->appKey;
$now = microtime(true) * 1000;
return $this->redis->eval($luaScript, 1, $key, $this->capacity, $this->rate, $now) === 1;
}
}
```
令牌桶负责把请求流量控制在平台允许的阈值内,熔断器则负责在第三方接口出现大面积报错时快速失败,避免无效请求持续占用系统资源。熔断器包含三种状态:关闭状态下统计连续错误次数,超过阈值就切换到打开状态,所有请求直接快速失败,等待冷却时间后进入半开状态,放行少量探测请求,如果请求成功就切回关闭状态,失败就继续保持打开状态。完整的状态机实现代码如下:
```php
class CircuitBreaker
{
private $redis;
private $appKey;
private $errorThreshold;
private $coolDownTime;
private $keyPrefix = '1688_api_breaker:';
const STATE_CLOSED = 0;
const STATE_OPEN = 1;
const STATE_HALF_OPEN = 2;
public function __construct($redis, $appKey, $errorThreshold = 15, $coolDownTime = 30)
{
$this->redis = $redis;
$this->appKey = $appKey;
$this->errorThreshold = $errorThreshold;
$this->coolDownTime = $coolDownTime;
}
public function allowRequest()
{
$key = $this->keyPrefix . $this->appKey;
$state = $this->redis->hget($key, 'state') ?: self::STATE_CLOSED;
if ($state == self::STATE_OPEN) {
$openTime = $this->redis->hget($key, 'open_time') ?: 0;
if (time() - $openTime > $this->coolDownTime) {
$this->redis->hset($key, 'state', self::STATE_HALF_OPEN);
return true;
}
return false;
}
if ($state == self::STATE_HALF_OPEN) {
$probeSuccess = $this->redis->hincrby($key, 'probe_success', 1);
if ($probeSuccess >= 1) {
$this->redis->hmset($key, 'state', self::STATE_CLOSED, 'error_count', 0);
}
return $probeSuccess <= 1;
}
return true;
}
public function reportResult($isSuccess)
{
$key = $this->keyPrefix . $this->appKey;
if (!$isSuccess) {
$errorCount = $this->redis->hincrby($key, 'error_count', 1);
if ($errorCount >= $this->errorThreshold) {
$this->redis->hmset($key, 'state', self::STATE_OPEN, 'open_time', time());
}
} else {
$this->redis->hset($key, 'error_count', 0);
}
}
}
```
不同环境下的配置参数可以按需调整,推荐的配置对比为:开发环境限流阈值每秒3次,熔断阈值连续错误3次,冷却时间10秒;测试环境限流阈值每秒10次,熔断阈值连续错误8次,冷却时间20秒;生产环境限流阈值每秒19次,熔断阈值连续错误15次,冷却时间30秒。
其实这套机制落地时还有几个高频坑点需要规避:第一,不要用本地内存存储令牌桶和熔断器状态,多实例分布式部署时状态不同步,很容易整体超限;第二,半开状态下放行的探测请求数量不要超过2个,防止下游接口刚恢复就被瞬间打挂;第三,订单全量同步的对账任务扫描间隔建议设置为5分钟,在同步时效和接口调用量之间找到最优平衡。
我当初上线前完全没注意到的一个暗坑:1688的公开文档只标注了每秒最多20次的调用限制,但新申请的AppKey实际还有一个没有对外公示的5分钟累计900次的软配额,哪怕你每秒调用量严格控制在阈值内,只要令牌桶以满速20次/秒连续跑满4分钟以上,依然会触发未公示的流控规则,严重时AppKey会被临时封禁长达24小时。
部署完成后可以分两步验证效果:先模拟每秒25次的并发请求压测,观察限流逻辑是否会自动拦截超出配额的请求,返回自定义的排队标识;再模拟下游接口连续返回500错误,观察熔断器是否会在连续15次错误后自动打开,30秒后自动放行探测请求,探测成功后恢复正常调用。最开始提到的所有做跨境代购系统后端开发的PHP开发者,都可以直接复用这套经过生产验证的方案,不需要从零开始踩坑。这套组合机制不光可以用于1688代采接口,对接物流商轨迹查询接口、跨境支付回调接口等所有不可控的第三方外部依赖,都可以直接复用这套逻辑,把外部接口故障的影响范围完全隔离,不会拖垮整个代购系统的主流程。