代购系统防薅指南:Redis+Lua 实现智能限流
代购系统防薅指南:Redis+Lua实现智能限流
代购系统跑起来后,最大的隐患往往不在业务逻辑,而在第三方接口。1688、淘宝、EMS物流——每个接口都有自己的QPS上限。促销高峰一来,采购请求集中爆发,轻则触发风控限流,重则账户被封。限流不是可选项,是生存线。
本文面向有1-3年后端经验、正在搭建或维护代购系统的开发者,重点解析令牌桶限流在Redis + Lua下的实现,以及在阿里云上的部署与运维。代码部分需有Redis和PHP基础,其他内容偏概念理解。
限流算法常见的有计数器、滑动窗口、漏桶、令牌桶四种。计数器最简单但无法应对突发流量;漏桶强迫匀速,但牺牲了峰值体验;令牌桶在两者之间找到平衡——允许一定程度的突发,同时保证长期速率恒定。
代购场景的特殊性在于:订单创建是瞬时高并发,但采购入库是异步的。令牌桶恰好适配这种"前端集中、后端分散"的模式。
Lua脚本实现令牌桶
Redis从2.6开始支持Lua脚本,原子性执行多个命令,天然适合限流这种读-算-写三步操作。
-- 令牌桶限流Lua脚本
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- 桶容量
local rate = tonumber(ARGV[2]) -- 每秒补充令牌数
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'last_time')
local tokens = tonumber(bucket[1]) or capacity
local last_time = tonumber(bucket[2]) or now
local elapsed = math.max(0, now - last_time)
local new_tokens = math.min(capacity, tokens + elapsed * rate)
local allowed = new_tokens >= requested
if allowed then
redis.call('HMSET', key, 'tokens', new_tokens - requested, 'last_time', now)
redis.call('EXPIRE', key, 60)
return 1
else
redis.call('HMSET', key, 'tokens', new_tokens, 'last_time', now)
redis.call('EXPIRE', key, 60)
return 0
end
这段脚本先计算距离上次请求补充了多少令牌,再判断能否取出足够的令牌完成本次请求。脚本本身是原子的,不存在并发竞争问题。
一个容易踩的坑:应用服务器和Redis服务器的时钟往往不同步,如果误差达到几百毫秒,在高并发场景下会导致令牌补充计算出现偏差。生产环境建议使用Redis服务器的时间(通过 redis.call('TIME') 获取),而不是直接传入应用层 time() 的值。
在Taocarts的采购模块中,调用这个脚本时传入不同的capacity和rate参数——1688接口给更小的桶和更低的速率,EMS查询接口可以适当放宽。参数化的设计让一套逻辑适配多个接口。
限流器的PHP封装
业务代码里不能每次都手写EVAL命令,封装一层通用类更实用:
class RateLimiter
{
private $redis;
public function __construct(Redis $redis)
{
$this->redis = $redis;
}
public function allow(string $key, int $capacity, float $rate): bool
{
$script = file_get_contents(__DIR__ . '/rate_limiter.lua');
$now = microtime(true);
$result = $this->redis->eval($script, 1,
$key, $capacity, $rate, $now, 1
);
return (bool) $result;
}
}
实际使用时,对接EMS物流接口设置 capacity=30, rate=10,每秒钟最多放行10个请求,桶容量30意味着允许短时突发到30个请求而不直接拒绝。这套配置比一刀切的全局限流灵活得多。
熔断兜底
限流只能控制自己的请求频率,但第三方接口可能随时抽风——超时、500、响应异常。连续失败后继续调用只会浪费资源、拖慢整体响应。这时候需要熔断器。
思路很简单:维护一个失败计数器,如果错误率超过阈值就"跳闸",后续请求直接返回降级结果而不是真的去请求。恢复后逐步放行,验证接口是否正常。这种模式在Taocarts对接多个物流商时尤为重要——一个通道挂了,业务不中断,自动切到备选通道。
这套方案在阿里云上的资源规划:
Redis推荐使用云数据库Redis版。自建Redis要处理主从切换、持久化配置、内存溢出,比业务代码还费心。云Redis本身的性能足够支撑每秒万级请求,还能和函数计算、ACK容器集群内网互通,延迟低、费用可控。
限流状态存储用Redis,持久化的业务数据放RDS MySQL。两者职责分离,避免限流操作抢占了订单查询的连接池资源。RDS可以开只读副本分离查询压力,费用比增大主实例规格要划算。
告警配置在CloudMonitor控制台设置两个规则:一是Redis CPU使用率超过70% 触发短信告警,二是限流拒绝率突然上升(意味着第三方接口可能变了策略)触发邮件通知。规则不要贪多,抓关键指标就够了。
日志分析通过SLS采集业务日志和Redis慢查询日志。大促前跑一次日志分析脚本,检查是否有异常IP集中请求、哪些接口被限流最多,作为下一轮容量规划的依据。
代购业务的流量特征是"脉冲式"——平时平稳,大促期间可能翻5-10倍。如果按峰值买ECS,平日浪费严重;如果按平时买,大促必定雪崩。
一个可行的思路是:平时用2台ECS跑应用层 + 1台RDS主库,大促前通过OOS自动化运维编排扩容到4台ECS + 1台RDS只读副本。OOS可以设置定时触发器,大促前2小时自动执行伸缩剧本,不用人肉盯着。CloudMonitor监控扩容后的服务状态,不健康就自动回滚。
Redis云服务本身按连接数和内存规格计费,选型时看业务实际使用的内存峰值,不要一开始就买顶配。大多数代购系统的限流状态数据很小,1GB足够。
写在最后
限流只是代购系统稳定性建设的一环。要真正扛住脉冲流量,还需要超时控制、重试策略、异步队列等多层防护。这些话题改天再展开。
当1688、淘宝、EMS这些第三方接口成为系统生死线时,令牌桶限流就是守护这条生存线的第一道屏障。
你在实际项目中遇到过第三方接口限流的坑吗?有没有更好的应对方案?欢迎在评论区聊聊。