边肖将与您分享如何用Redis实现令牌桶算法。相信大部分人还不太了解,所以分享这篇文章给大家参考。希望你看完这篇文章会有很多收获。我们一起来看看吧!
在限流算法中,有一种令牌桶算法,可以处理短时突发流量,特别适用于真实环境中流量不均匀的情况。不会频繁触发限流,对来电者友好。
比如限流是10qps,大部分情况下不会超过,但偶尔会达到30qps,然后很快恢复正常。假设这种流量的爆发不会影响系统的稳定性,我们可以在一定程度上允许这种瞬间的流量爆发,从而给用户带来更好的可用性体验。这是使用令牌桶算法的地方。
令牌桶算法原理
如下图所示,这个算法的基本原理是有一个容量为x的令牌桶,每y个单位时间就有z个令牌放入这个桶中。如果桶中的令牌数超过x,将被丢弃。处理请求时,需要先从令牌桶中取出令牌,如果拿到令牌,继续处理;如果令牌不可用,请求将被拒绝。
可见,在令牌桶算法中设置x、y、z的个数尤为重要。z应该略大于y单位时间的请求数,系统会长期处于这种状态;x是系统允许的瞬时最大请求数,系统不宜长时间处于这种状态,否则会频繁触发限流,说明流量超出预期,需要及时调查原因并采取相应措施。
00-1010之前,看到一些程序实现的令牌桶。将令牌放入桶中的方法是启动一个线程,每y个单位时间增加一次令牌的数量,或者在Timer中定期执行这个过程。我对这种方法不满意。原因有二。首先,它浪费线程资源。二是因为调度问题在执行时间上不准确。【相关推荐:Redis视频教程】
这里,计算确定令牌桶中令牌数量的方法。首先,从最后一个请求到这个请求过去了多长时间,是否达到了发放令牌的时间阈值,然后添加了多少令牌,可以将多少令牌放入桶中。
说话便宜!
让我们看看如何在Redis中实现它,因为它涉及到与Redis的许多交互。为了提高限流处理的吞吐量,减少程序与Redis的交互次数,采用了Redis支持的Lua脚本,Lua脚本的执行是原子性的,不需要担心脏数据。
代码节选自FireflySoft。RateLimit,不仅支持Redis的普通主从部署,还支持集群Redis,所以通过横向扩展可以提高吞吐量。为了阅读方便,这里加了一些注释,其实是没有的。
-定义返回值,为数组,包括:是否触发限流(1限流0遍)以及当前桶中的令牌数。
localret={}
ret[1]=0
- Redis集群分区Key,KEYS[1]为限流目标。
localcl_key='{ '.钥匙[1].'}'
-获取限流处罚的当前设置,当触发限流处罚时,会写入一个有到期时间的KV。
-如果有电流限制惩罚,则返回结果[1,-1]。
locallock_key=cl_key.-锁
locallock_val=redis.call('get ',lock_key)
如果iflock _ val==' 1 ’,则
ret[1]=1
ret[2]=-1
returnret
结束
-这里省略了部分代码。
-获取[上次将令牌放入桶中的时间]。如果未设置此时间,则令牌桶不存在。这时:
-一种情况是:第一次执行,此时定义的令牌桶已满。
-另一种情况是过流限制处理很长时间没有进行,导致这次KV携带的释放。
-此过期时间将超过令牌自然放入桶中的时间,直到桶满为止,因此令牌桶也应该满了。
locallast_time=redis.call('get ',st_key)
if(last_time==false)
然后
-此执行后剩余的令牌数:存储桶容量-此执行中消耗的令牌数。
bucket _ amount=产能-金额;
-更新令牌桶中的令牌数量,这里有一个到期时间。如果该程序长时间不执行,令牌桶KV将被回收。
ll('set',KEYS[1],bucket_amount,'PX',key_expire_time)
-- 设置[上次向桶中放入令牌的时间],后边计算应放入桶中的令牌数量时会用到
redis.call('set',st_key,start_time,'PX',key_expire_time)
-- 返回值[当前桶中的令牌数]
ret[2]=bucket_amount
-- 无需其它处理
return ret
end
-- 令牌桶存在,获取令牌桶中的当前令牌数
local current_value = redis.call('get',KEYS[1])
current_value = tonumber(current_value)
-- 判断是不是该放入新令牌到桶中了:当前时间-上次投放的时间 >= 投放的时间间隔
last_time=tonumber(last_time)
local last_time_changed=0
local past_time=current_time-last_time
if(past_time<inflow_unit)
then
-- 不到投放的时候,直接从令牌桶中取走令牌
bucket_amount=current_value-amount
else
-- 需要放入一些令牌, 预计投放数量 = (距上次投放过去的时间/投放的时间间隔)*每单位时间投放的数量
local past_inflow_unit_quantity = past_time/inflow_unit
past_inflow_unit_quantity=math.floor(past_inflow_unit_quantity)
last_time=last_time+past_inflow_unit_quantity*inflow_unit
last_time_changed=1
local past_inflow_quantity=past_inflow_unit_quantity*inflow_quantity_per_unit
bucket_amount=current_value+past_inflow_quantity-amount
end
-- 这里省略部分代码
ret[2]=bucket_amount
-- 如果桶中剩余数量小于0,则看看是否需要限流惩罚,如果需要则写入一个惩罚KV,过期时间为惩罚的秒数
if(bucket_amount<0)
then
if lock_seconds>0 then
redis.call('set',lock_key,'1','EX',lock_seconds,'NX')
end
ret[1]=1
return ret
end
-- 来到这里,代表可以成功扣减令牌,则需要更新令牌桶KV
if last_time_changed==1 then
redis.call('set',KEYS[1],bucket_amount,'PX',key_expire_time)
-- 有新投放,更新[上次投放时间]为本次投放时间
redis.call('set',st_key,last_time,'PX',key_expire_time)
else
redis.call('set',KEYS[1],bucket_amount,'PX',key_expire_time)
end
return ret
通过以上代码,可以看出,其主要处理过程是:
1、判断有没有被限流惩罚,有则直接返回,无则进入下一步。
2、判断令牌桶是否存在,不存在则先创建令牌桶,然后扣减令牌返回,存在则进入下一步。
3、判断是否需要投放令牌,不需要则直接扣减令牌,需要则先投放令牌再扣减令牌。
4、判断扣减后的令牌数,如果小于0则返回限流,同时设置限流惩罚,如果大于等于0则进入下一步。
5、更新桶中的令牌数到Redis。
你可以在任何一种开发语言的Redis库中提交并运行这段Lua script脚本,如果你使用的是.NET平台,可以参考这篇文章:ASP.NET Core中使用令牌桶限流(https://blog.bossma.cn/dotnet/asp-net-core-token-bucket-algorithm-of-rate-limit/) 。
关于FireflySoft.RateLimit
FireflySoft.RateLimit 是一个基于 .NET Standard 的限流类库,其内核简单轻巧,能够灵活应对各种需求的限流场景。
其主要特点包括:
-
多种限流算法:内置固定窗口、滑动窗口、漏桶、令牌桶四种算法,还可自定义扩展。
-
多种计数存储:目前支持内存、Redis两种存储方式。
-
分布式友好:通过Redis存储支持分布式程序统一计数。
-
限流目标灵活:可以从请求中提取各种数据用于设置限流目标。
-
支持限流惩罚:可以在客户端触发限流后锁定一段时间不允许其访问。
-
动态更改规则:支持程序运行时动态更改限流规则。
-
自定义错误:可以自定义触发限流后的错误码和错误消息。
-
普适性:原则上可以满足任何需要限流的场景。
以上是“利用Redis如何实现令牌桶算法”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注行业资讯频道!
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/154052.html