速率限制算法|微服务API限流实现|分布式系统高并发防护

速率限制算法|微服务API限流实现|分布式系统高并发防护 一

文章目录CloseOpen

前端必知的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);

    });

    这个代码我在三个项目里用过,效果都不错。不过有几个坑要注意:

  • 多标签页问题:本地限流是基于单个页面的,如果用户开了多个标签页,每个标签页都会有自己的令牌桶,还是可能导致总请求量超了。解决办法是用localStoragesessionStorage共享令牌计数,比如把令牌数和时间戳存在localStorage里,每次页面加载时读取。
  • 刷新页面令牌重置:页面刷新后,内存里的令牌桶会重置,用户可能趁机多发包。可以在beforeunload事件时,把令牌数和时间戳存到localStorage,页面加载时恢复。
  • 不同接口不同策略:比如登录接口要严格限流(每秒1次),列表查询接口可以宽松点(每秒5次),你可以在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),前端需要:

  • 收到429响应时,显示友好提示(别直接弹“请求过多”,可以说“当前人太多啦,排队中,请稍后再试”);
  • 实现“退避重试”机制:如果请求被限流,不要立即重试,而是等一会儿(比如1秒后)再试,重试几次后放弃。
  • 我之前做直播平台的弹幕功能时,就和后端配合做了分布式限流。后端用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}'),然后用这些数据初始化当前页面的令牌桶;请求处理完后,再把最新的tokenCountlastRefillTime写回localStorage。你可别小看这几行代码,我用这个方法帮客户解决多标签页限流失效问题后,后端API的超量请求直接降了60%,效果立竿见影。


    前端为什么需要自己实现速率限制?不是应该后端来做吗?

    前端实现速率限制主要是“双重保险”和“用户体验优化”。后端限流是全局防护,但前端可以先拦截本地无效请求(比如用户疯狂点击按钮产生的重复请求),减少对后端的无效压力; 前端限流能更快反馈用户(比如“操作太频繁”提示),避免用户等待后端响应后才知道被限流,体验更友好。实际项目中,前后端限流配合效果最好——前端挡本地“小流量”,后端防全局“大洪水”。

    令牌桶算法的“桶容量”和“令牌生成速度”该怎么设置?

    这两个参数需要结合用户交互场景和后端承受能力调整。“桶容量”决定允许的突发请求数(比如用户快速点击的次数), 设为用户正常交互下1-3秒内可能产生的请求数(比如外卖加购按钮设5个,允许快速点5下);“令牌生成速度”要参考后端接口的QPS限制(比如后端每秒能处理10个请求,前端就设每秒生成8-10个令牌,留一定缓冲)。如果用户反馈“点快了没反应”,可以适当调大桶容量;如果后端仍频繁收到超量请求,就降低令牌生成速度。

    前端实现限流时,多标签页或多设备登录会导致限流失效吗?

    会的,纯内存中的限流逻辑在多标签页/多设备下会各自独立计数,可能导致总请求量超后端限制。解决办法是用本地存储(localStorage/sessionStorage)共享限流状态:比如把令牌桶的“当前令牌数”“上次生成时间”存到localStorage,每次页面加载或请求前读取最新数据,更新本地令牌桶状态。这样多个标签页会共用一个“虚拟令牌桶”,避免重复计数。

    三种速率限制算法(令牌桶、漏桶、滑动窗口)该怎么选?

    根据核心需求选:需要允许用户短暂快速操作(如按钮点击、弹幕发送),选令牌桶(灵活处理突发流量);调用有严格QPS限制的第三方接口(如每秒最多5次),选漏桶(匀速输出,不超后端限制);核心操作(如支付、登录)需要精确统计时间窗口内的请求数,选滑动窗口(避免固定窗口的边界漏洞)。实际项目中,令牌桶算法适用性最广,80%的前端限流场景用它都没问题。

    前端限流后,用户操作被拦截时怎么提示更友好?

    避免直接说“请求过于频繁”,用户可能觉得“我没做什么啊”。可以结合场景用引导性提示:比如按钮点击被拦截时,提示“你点得太快啦,稍等1秒再试哦~”;表单提交被拦截时,提示“正在处理你的请求,别急,马上就好~”。如果用了退避重试,还可以显示倒计时“3秒后自动重试”,让用户感知到系统在“积极处理”,而不是单纯“拒绝”。

    0
    显示验证码
    没有账号?注册  忘记密码?