前段时间用jwt实现了邮箱验证码的校验和用户认证登录,专门写了一篇文章作为总结。在那篇文章中提到了一个点,如何限制速度。对于短信验证码和邮件验证码,如果没有限速的话,就会被恶意攻击造成大量的QPS,不仅会拖累服务,还会造成心疼的资费。鉴于君子可以扶贫的原则,我的电子邮件服务被添加了速度限制。关于如何限速,有两种比较著名的算法,漏桶算法和令牌桶算法。这里简单介绍一下,最后在我发邮件的API中练习一下。下面是发送邮件的API,已经限制为一分钟两次,大家可以通过修改email进行实验。您也可以直接使用curl'https://graphql.xiang.tech/graphql'-H'Content-Type:application/json'--data-binary'{"query":"mutationSEND($email:String!){\nsendEmailVerifyCode(email:$email)\n}","variables":{"email":"xxxxxx@qq.com"}}'下面是我的登录实践系列文章【登录那些事儿】实现MaterialDesign的登录样式【登录那些事】使用jwt登录验证验证码【登录那些事】邮件发送、限流、漏桶和令牌桶本文地址:https://shanyue.tech/post/rat...LeakyBucket(漏桶算法)漏桶算法是指水滴(请求)先进入漏桶,漏桶(水桶)以一定的速度流出。当漏桶里的水满了,就不能再加水了。维护一个计数器作为一个桶,计数器的上限为桶的大小当计数器满时拒绝请求每隔一段时间清空一次计数器使用option表示在option的窗口时间内至多option.max请求.window下面是使用redis的计数器实现限流伪码constoption={max:10,//窗口时间限制10个请求window:1000//1s}functionaccess(req){//生成唯一flag根据请求constkey=identity(req)//计数器递增constcounter=redis.incr(key)if(counter===1){//如果是当前时间窗口的第一个请求,设置过期时间redis.expire(key,window)}if(counter>option.window){returnfalse}returntrue}这里是Redis官方关于使用INCR实现限流的文档https://redis.io/commands/INCR这时候有一个问题不是问题,就是它的时间窗口,它不像滑动窗口,一个球从桶里出来,另一个球可以进来。相反,它更像是一个固定的时间一堆球从桶里出来然后开始得分的窗口。正因为如此,它可能会在固定窗口的后半段收到max-1个请求,然后在下一个固定窗口发出max个请求。此时一个随机窗口时间请求中最多会有2*max-1个请求。另外redis有一个INCR和EXPIRE的原子性问题,容易造成RaceCondition,可以通过SETNX来解决。redis.set(key,0,'EX',option.window,'NX')也可以通过一个LUAScript来解决,显然SETNX更简单localcurrent=redis.call("incr",KEYS[1])iftonumber(current)==1thenredis.call("expire",KEYS[1],1)end为了解决2N问题,可以从维护一个计数器改为维护一个队列。代价是内存占用太高,解决RaceCondition难度更大。下面是使用redis的set/getstringconstoption={max:10,//限速为窗口内10个请求window:1000//1s}functionaccess(req){//生成一个uniqueflag根据请求constkey=identity(req)constcurrent=Date.now()//cache视为一个缓存对象//过滤掉当前时间窗口内的请求数,每一个请求标记为一个timestampformat//为了简单起见,这里不做json的序列化和反序列化...consttimestamps=[current].concat(redis.get('timestamps')).filter(ts=>ts+option.window>current)if(timestamps.length>option.max){returnfalse}//此时读写不同步,会出现RaceCondition问题redis.set('timestamps',timestamps,'EX',option.window)returntrue}这里我们使用LUA脚本来解决RaceConditionTODOTokenBucket(令牌桶算法)的问题首先看一下令牌桶和漏桶的区别。令牌桶的初始状态是满的,漏桶的初始状态为空。令牌桶在桶为空时拒绝新请求,漏桶在桶满时拒绝新请求。当一个请求来的时候,假设一个请求消耗了一个token,令牌桶桶减少一个token,漏桶增加一个token。使用redis实现令牌桶TODO