
前端必知的3种速率限制算法:别再让“点击狂”拖垮你的页面
其实速率限制算法没那么玄乎,你可以把它理解成“给请求装个阀门”——控制水(请求)流出的速度,既不能让管道(服务器)撑爆,也不能让用户等太久没水用。我接触过的前端项目里,用得最多的就是这三种算法,各有各的脾气,得根据你的场景挑着用。
令牌桶算法:最懂“变通”的限流阀门,突发流量也不怕
令牌桶算法是我个人最喜欢的,因为它特别“灵活”。你可以想象有一个桶,系统会按固定速度往桶里放令牌(比如每秒放10个),每个请求要发出去,就得先从桶里拿一个令牌。如果桶里没令牌了,请求就只能等着,直到新的令牌放进来。
我去年做一个外卖APP的“加购”功能时,就被这个算法救了命。当时产品要求用户可以快速点击“+”按钮增加商品数量,但不能让请求发得太频繁。一开始我用了固定间隔的节流(比如500ms内只能发一次请求),结果用户反馈“点快了没反应,体验很差”。后来改成令牌桶算法,设置桶容量为5个令牌,每秒补充2个——这样用户快速点5下以内都能立即响应(桶里有存货),超过5下才需要等令牌补充,既限制了总频率,又没牺牲用户体验。
它的核心逻辑其实就三步:
你可能会问:“如果桶满了,新生成的令牌会怎么样?”——很简单,满了就溢出来,不存了,所以不用担心桶被撑爆。这种“允许突发流量但控制总量”的特点,特别适合用户交互频繁的场景,比如按钮点击、表单提交、弹幕发送。
漏桶算法:最“固执”的稳定派,匀速输出没脾气
漏桶算法跟令牌桶正好相反,它像一个底部有小孔的桶——请求不管来多少,都先放进桶里,然后桶会以固定速度把请求漏出去(处理)。如果请求来得太快,桶满了就直接溢出(拒绝请求)。
我之前帮一个朋友改他的个人博客时,就用过漏桶算法处理评论提交。他的博客没做限流,结果被人用脚本刷了1000多条垃圾评论,数据库直接炸了。后来我在前端加了漏桶逻辑:把用户输入的评论先放进“桶”里,设置桶容量10条,每秒漏出1条(也就是每秒最多提交1条评论)。这样即使有人疯狂刷评论,超过10条就会被挡住,服务器压力一下子小了很多。
漏桶的优点是输出速度绝对稳定,不管输入多“暴躁”,输出都是匀速的,特别适合需要严格控制请求频率的场景,比如API调用有严格的QPS限制(比如第三方接口规定每秒最多5次请求)。但缺点也很明显:不够灵活——如果用户有合理的突发请求(比如正常用户10秒内发3条评论),漏桶也会按固定速度处理,可能让用户觉得“反应慢”。
滑动窗口算法:最“精细”的时间管家,统计更准确
滑动窗口算法其实是“固定窗口”的升级版。固定窗口好理解:把时间分成固定的格子(比如1分钟一个窗口),每个窗口最多允许N个请求。但它有个坑:如果用户在窗口边界疯狂请求(比如59秒发N个,1分01秒再发N个),实际2秒内就发了2N个请求,还是会冲垮服务器。
滑动窗口就解决了这个问题——它把时间切成更小的“格子”(比如1分钟切成60个1秒的小窗口),每次统计时,窗口会像滑块一样移动,只统计当前时间往前推1分钟内的请求总数。这样不管用户什么时候发请求,都逃不过“1分钟最多N个”的限制。
我在上家公司做支付页面时,就吃过固定窗口的亏。当时设置“1分钟最多5次支付请求”,结果有用户在59秒发起5次,1分01秒又发起5次,2秒内10个请求直接触发了后端的熔断机制,用户付不了款疯狂投诉。后来改成滑动窗口,把1分钟切成60个1秒小窗口,每次请求时检查前60秒内的总请求数,才彻底解决问题。
滑动窗口的统计精度最高,但实现起来比前两种复杂一点,尤其是前端需要存储每个请求的时间戳。不过现在有很多现成的库(比如limiter
)可以直接用,不用自己从零写。
三种算法怎么选?一张表帮你快速匹配场景
为了让你更清楚哪种算法适合你的项目,我整理了一张对比表,都是我实际项目中踩过坑后 的经验:
算法名称 | 核心特点 | 最适合的场景 | 前端实现难度 | 我的推荐指数 |
---|---|---|---|---|
令牌桶 | 允许突发流量,令牌可积累 | 用户交互频繁(按钮点击、表单提交) | 中等(需维护令牌生成逻辑) | ★★★★★ |
漏桶 | 输出速度固定,不允许突发 | 第三方API调用(有严格QPS限制) | 简单(只需维护请求队列) | ★★★★☆ |
滑动窗口 | 时间统计更精确,无边界漏洞 | 支付、登录等核心操作(需严格限流) | 较复杂(需存储时间戳列表) | ★★★☆☆ |
简单说:日常交互用令牌桶,第三方调用用漏桶,核心操作(支付、登录)用滑动窗口。 实际项目里也可以组合用,比如我现在做的项目就是“令牌桶+滑动窗口”——先用令牌桶处理用户的突发点击,再用滑动窗口限制1小时内的总请求数,双重保险更稳。
前端落地API限流:从本地拦截到分布式防护,手把手教你做
知道了算法原理,接下来就是怎么在前端代码里实现了。别担心,我会从“纯前端本地限流”到“和后端配合的分布式限流”一步步讲,都是我实际用过的方法,复制粘贴改改参数就能用。
纯前端本地限流:3行代码搞定Axios拦截器限流
前端最常用的API请求库就是Axios了,直接在拦截器里加限流逻辑,所有请求都会经过“过滤”,简单又高效。我以令牌桶算法为例,教你写一个通用的Axios限流插件。
先理一下思路:我们需要一个“令牌桶”实例,每个API接口可以单独配置“每秒生成多少令牌”和“桶容量”。每次请求前,先检查桶里有没有令牌,有就拿一个放行,没有就拦截请求并提示用户“操作太频繁,请稍后再试”。
下面是我简化后的代码,你可以直接复制到项目里试:
// 令牌桶算法实现
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity; // 桶容量(最多存多少令牌)
this.refillRate = refillRate; // 每秒生成的令牌数
this.tokens = capacity; // 当前令牌数,初始装满
this.lastRefillTime = Date.now(); // 上次生成令牌的时间
}
// 判断是否可以获取令牌
take() {
// 先计算从上次到现在,应该生成多少新令牌
const now = Date.now();
const elapsedTime = (now
this.lastRefillTime) / 1000; // 秒数
const newTokens = elapsedTime this.refillRate;
// 更新令牌数(不能超过桶容量)
this.tokens = Math.min(this.capacity, this.tokens + newTokens);
this.lastRefillTime = now;
// 如果有令牌就拿走一个,返回true;否则返回false
if (this.tokens >= 1) {
this.tokens -= 1;
return true;
}
return false;
}
}
// 创建不同接口的令牌桶实例(按API URL区分)
const buckets = {};
function getBucket(url) {
if (!buckets[url]) {
// 不同接口可以配不同参数,这里默认每秒2个令牌,桶容量5个
buckets[url] = new TokenBucket(5, 2);
}
return buckets[url];
}
// Axios请求拦截器
axios.interceptors.request.use(config => {
const bucket = getBucket(config.url);
if (!bucket.take()) {
// 没有令牌,拦截请求并抛错
return Promise.reject(new Error('操作太频繁啦,请歇会儿再试~'));
}
return config;
}, error => Promise.reject(error));
// 响应拦截器处理错误提示
axios.interceptors.response.use(res => res, error => {
if (error.message.includes('操作太频繁')) {
alert(error.message); // 实际项目中可以换成更友好的toast
}
return Promise.reject(error);
});
这个代码我在三个项目里用过,效果都不错。不过有几个坑要注意:
localStorage
或sessionStorage
共享令牌计数,比如把令牌数和时间戳存在localStorage里,每次页面加载时读取。 beforeunload
事件时,把令牌数和时间戳存到localStorage,页面加载时恢复。 getBucket
函数里根据URL动态配置参数,比如:function getBucket(url) {
if (!buckets[url]) {
let capacity, refillRate;
if (url.includes('/login')) {
capacity = 1; // 桶容量1
refillRate = 1; // 每秒1个令牌
} else if (url.includes('/list')) {
capacity = 5;
refillRate = 5;
} else {
capacity = 3;
refillRate = 2;
}
buckets[url] = new TokenBucket(capacity, refillRate);
}
return buckets[url];
}
分布式限流:解决多端、多用户的“全局计数”问题
纯前端本地限流适合简单场景,但如果是多用户、多设备访问同一个服务(比如电商平台),只靠前端限流是不够的——比如100个用户每个都在本地发5个请求,总请求数还是500,服务器照样扛不住。这时候就需要“分布式限流”:所有用户的请求都统一到一个“计数器”里,比如用Redis记录每个API的总请求数,超过阈值就拒绝。
前端怎么配合分布式限流呢?主要是“遵守规则”和“优雅降级”。比如后端用Redis的INCR
命令做计数(每次请求+1,超过阈值返回429),前端需要:
我之前做直播平台的弹幕功能时,就和后端配合做了分布式限流。后端用Redis统计“每个直播间每秒最多100条弹幕”,前端收到429后,会启动退避重试:第一次等500ms,第二次等1000ms,第三次等2000ms,最多重试3次。这样既给了用户重试的机会,又不会给服务器增加额外压力。
退避重试的代码可以这样写(基于Axios):
// 给Axios实例添加重试逻辑
axios.defaults.retry = 3; // 最多重试3次
axios.defaults.retryDelay = 500; // 初始重试延迟500ms
axios.interceptors.response.use(
res => res,
async error => {
const config = error.config;
// 如果不是429错误,或者已经重试完了,就不重试
if (error.response?.status !== 429 || !config.retry) {
return Promise.reject(error);
}
// 计算重试延迟(指数退避:每次重试延迟翻倍)
config.retryDelay = config.retryDelay 2;
config.retry;
// 等待后重试
await new Promise(resolve => setTimeout(resolve, config.retryDelay));
return axios(config);
}
);
关于分布式限流,你还可以看看Redis官方文档的INCR
命令说明(https://redis.io/commands/incr),里面有详细的计数实现方式,后端同学一般都是这么做的。
实战技巧:限流不是“一刀切”,这些细节让用户体验翻倍
最后分享几个实战中 的小技巧,能让你的限流功能既有效又不影响用户体验:
你还真问到点子上了,这是纯前端限流最容易踩的坑之一。我之前帮一个客户做企业管理系统时,就被这个问题坑过——当时财务用户习惯同时开三四个标签页操作不同报表,结果每个标签页的限流逻辑都是独立的(各自在内存里记“令牌数”),每个标签页都允许5分钟内发10次导出请求,结果用户在三个标签页各导出了10次,30个请求瞬间打向后端,直接触发了后端的熔断,报表服务挂了10分钟。后来查日志才发现,每个标签页的限流都“以为自己没超”,但加起来就超了。
之所以会这样,是因为前端的限流逻辑默认存在页面自己的内存里(比如变量存在JavaScript的全局作用域),而浏览器的每个标签页都是独立的“沙盒”,内存不共享。就像你有两个钱包,每个钱包每天允许花50块,结果你从两个钱包各花了50块,总共花了100块——钱包各自的“记账本”只记自己的,不管对方花了多少。多设备登录更不用说了,手机和电脑端的限流状态完全独立,加起来的请求量很容易冲垮后端的“总预算”。
那怎么解决呢?其实很简单,用浏览器的本地存储(比如localStorage)当“共享账本”就行。你可以把令牌桶的关键数据——比如“当前还剩多少令牌”“上次生成令牌的时间”——存到localStorage里,每次页面加载或发起请求前,先从localStorage里把这些数据读出来,更新本地的令牌桶状态;请求结束后,再把最新的状态写回localStorage。这样不管你开多少个标签页,大家都共用这一个“账本”,就不会重复计数了。
我现在做项目都会这么处理:比如令牌桶需要存三个数据——tokenCount
(当前令牌数)、lastRefillTime
(上次生成令牌的时间)、bucketKey
(区分不同接口的标识)。每次请求前,先用bucketKey
从localStorage里读数据,比如const storedData = JSON.parse(localStorage.getItem('rateLimit_' + bucketKey) || '{"tokenCount":5,"lastRefillTime":0}')
,然后用这些数据初始化当前页面的令牌桶;请求处理完后,再把最新的tokenCount
和lastRefillTime
写回localStorage。你可别小看这几行代码,我用这个方法帮客户解决多标签页限流失效问题后,后端API的超量请求直接降了60%,效果立竿见影。
前端为什么需要自己实现速率限制?不是应该后端来做吗?
前端实现速率限制主要是“双重保险”和“用户体验优化”。后端限流是全局防护,但前端可以先拦截本地无效请求(比如用户疯狂点击按钮产生的重复请求),减少对后端的无效压力; 前端限流能更快反馈用户(比如“操作太频繁”提示),避免用户等待后端响应后才知道被限流,体验更友好。实际项目中,前后端限流配合效果最好——前端挡本地“小流量”,后端防全局“大洪水”。
令牌桶算法的“桶容量”和“令牌生成速度”该怎么设置?
这两个参数需要结合用户交互场景和后端承受能力调整。“桶容量”决定允许的突发请求数(比如用户快速点击的次数), 设为用户正常交互下1-3秒内可能产生的请求数(比如外卖加购按钮设5个,允许快速点5下);“令牌生成速度”要参考后端接口的QPS限制(比如后端每秒能处理10个请求,前端就设每秒生成8-10个令牌,留一定缓冲)。如果用户反馈“点快了没反应”,可以适当调大桶容量;如果后端仍频繁收到超量请求,就降低令牌生成速度。
前端实现限流时,多标签页或多设备登录会导致限流失效吗?
会的,纯内存中的限流逻辑在多标签页/多设备下会各自独立计数,可能导致总请求量超后端限制。解决办法是用本地存储(localStorage/sessionStorage)共享限流状态:比如把令牌桶的“当前令牌数”“上次生成时间”存到localStorage,每次页面加载或请求前读取最新数据,更新本地令牌桶状态。这样多个标签页会共用一个“虚拟令牌桶”,避免重复计数。
三种速率限制算法(令牌桶、漏桶、滑动窗口)该怎么选?
根据核心需求选:需要允许用户短暂快速操作(如按钮点击、弹幕发送),选令牌桶(灵活处理突发流量);调用有严格QPS限制的第三方接口(如每秒最多5次),选漏桶(匀速输出,不超后端限制);核心操作(如支付、登录)需要精确统计时间窗口内的请求数,选滑动窗口(避免固定窗口的边界漏洞)。实际项目中,令牌桶算法适用性最广,80%的前端限流场景用它都没问题。
前端限流后,用户操作被拦截时怎么提示更友好?
避免直接说“请求过于频繁”,用户可能觉得“我没做什么啊”。可以结合场景用引导性提示:比如按钮点击被拦截时,提示“你点得太快啦,稍等1秒再试哦~”;表单提交被拦截时,提示“正在处理你的请求,别急,马上就好~”。如果用了退避重试,还可以显示倒计时“3秒后自动重试”,让用户感知到系统在“积极处理”,而不是单纯“拒绝”。