成功多规则限流的思索与通常-Redis (成功的规则)
简介
市面上很多引见redis如何成功限流的,然而大局部都有一个缺陷,就是只能成功繁多的限流,比如1分钟访问1次或许60分钟访问10次这种,然而假构想一个接口两种规则都须要满足呢,咱们的名目又是散布式名目,应该如何处置,上方就引见一下redis成功散布式多规则限流的形式。
思索
处置方法
记载某IP访问次数
经常使用String结构记载固定期间段内某用户IP访问某接口的次数
阻拦恳求:
剖析:规则是每分钟访问1000次
假定目前RedisKey=>RedisValue为999
目前少量恳求启动到第一步(失掉Redis恳求次数),那么一切线程都失掉到了值为999,启动判别都未超越限定次数则不阻拦,造成实践次数超越1000次
「处置方法:」保障方法口头原子性(加锁、lua)
图片
代码成功:比拟便捷
参考:。
Zset处置临界值疑问
经常使用Zset启动存储,处置临界值访问疑问
图片
网上简直都有成功,这里就不过多引见
成功多规则限流
先确定最终须要的成果
经过以上要求设计注解(先构想出最终成成成果)
@RateLimiter(rules={//60秒内只能访问10次@RateRule(count=10,time=60,timeUnit=TimeUnit.SECONDS),//120秒内只能访问20次@RateRule(count=20,time=120,timeUnit=TimeUnit.SECONDS)},//防重复提交(5秒钟只能访问1次)preventDuplicate=true)
编写注解(RateLimiter,RateRule)
编写RateLimiter注解。
/***@Description:恳求接口限制*@Author:yiFei*/@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic@interfaceRateLimiter{/***限流key*/Stringkey()defaultRedisKeyConstants.RATE_LIMIT_CACHE_PREFIX;/***限流类型(自动Ip形式)*/LimitTypeEnumlimitType()defaultLimitTypeEnum.IP;/***失误揭示*/ResultCodemessage()defaultResultCode.REQUEST_MORE_ERROR;/***限流规则(规则无法变,可多规则)*/RateRule[]rules()default{};/***防重复提交值*/booleanpreventDuplicate()defaultfalse;/***防重复提交自动值*/RateRulepreventDuplicateRule()default@RateRule(count=1,time=5);}
编写RateRule注解
@Target(ElementType.ANNOTATION_TYPE)@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic@interfaceRateRule{/***限流次数*/longcount()default10;/***限流期间*/longtime()default60;/***限流期间单位*/TimeUnittimeUnit()defaultTimeUnit.SECONDS;}
阻拦注解RateLimiter
RedisKey=prefix:className:methodName
RedisScore=期间戳
RedisValue=恣意散布式不重复的值即可
/***经过rateLimiter和joinPoint拼接prefix:ip/userId:classSimpleName-methodName**@paramrateLimiter提供prefix*@paramjoinPoint提供classSimpleName:methodName*@return*/publicStringgetCombineKey(RateLimiterrateLimiter,JoinPointjoinPoint){StringBufferkey=newStringBuffer(rateLimiter.key());//不同限流类型经常使用不同的前缀switch(rateLimiter.limitType()){//XXX可以新增经过参数指定参数启动限流caseIP:key.end(IpUtil.getIpAddr(((ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest())).append(":");break;caseUSER_ID:SysUserDetailsuser=SecurityUtil.getUser();if(!ObjectUtils.isEmpty(user))key.append(user.getUserId()).append(":");break;caseGLOBAL:break;}MethodSignaturesignature=(MethodSignature)joinPoint.getSignature();Methodmethod=signature.getMethod();Class<?>targetClass=method.getDeclaringClass();key.append(targetClass.getSimpleName()).append("-").append(method.getName());returnkey.toString();}
编写lua脚本
编写lua脚本(两种将期间减少到Redis的方法)。
Zset的UUIDvalue值
UUID(可用其余有相反的个性的值)为Zset中的value值
KEYS[1]=prefix:?:className:methodName
KEYS[2]=惟一ID
KEYS[3]=以后期间
ARGV=[次数,单位期间,次数,单位期间,次数,单位期间...]
--1.失掉参数localkey=KEYS[1]localuuid=KEYS[2]localcurrentTime=tonumber(KEYS[3])--2.以数组最大值为ttl最大值localexpireTime=-1;--3.遍历数组检查能否超越限流规则fori=1,#ARGV,2dolocalrateRuleCount=tonumber(ARGV[i])localrateRuleTime=tonumber(ARGV[i+1])--3.1判别在单位期间内访问次数localcount=redis.call('ZCOUNT',key,currentTime-rateRuleTime,currentTime)--3.2判别能否超越规则次数iftonumber(count)>=rateRuleCountthenreturntrueend--3.3判别元素最大值,设置为最终过时期间ifrateRuleTime>expireTimethenexpireTime=rateRuleTimeendend--4.redis中减少以后期间redis.call('ZADD',key,currentTime,uuid)--5.降级缓存过时期间redis.call('PEXPIRE',key,expireTime)--6.删除最大期间限制之前的数据,防止数据过多redis.call('ZREMRANGEBYSCORE',key,0,currentTime-expireTime)returnfalse
依据期间戳作为Zset中的value值
KEYS[1]=prefix:?:className:methodName
KEYS[2]=以后期间
ARGV=[次数,单位期间,次数,单位期间,次数,单位期间...]
--1.失掉参数localkey=KEYS[1]localcurrentTime=KEYS[2]--2.以数组最大值为ttl最大值localexpireTime=-1;--3.遍历数组检查能否越界fori=1,#ARGV,2dolocalrateRuleCount=tonumber(ARGV[i])localrateRuleTime=tonumber(ARGV[i+1])--3.1判别在单位期间内访问次数localcount=redis.call('ZCOUNT',key,currentTime-rateRuleTime,currentTime)--3.2判别能否超越规则次数iftonumber(count)>=rateRuleCountthenreturntrueend--3.3判别元素最大值,设置为最终过时期间ifrateRuleTime>expireTimethenexpireTime=rateRuleTimeendend--4.降级缓存过时期间redis.call('PEXPIRE',key,expireTime)--5.删除最大期间限制之前的数据,防止数据过多redis.call('ZREMRANGEBYSCORE',key,0,currentTime-expireTime)--6.redis中减少以后期间(处置多个线程在同一毫秒减少相反value造成Redis漏记的疑问)--6.1maxRetries最大重试次数retries重试次数localmaxRetries=5localretries=0whiletruedolocalresult=redis.call('ZADD',key,currentTime,currentTime)ifresult==1then--6.2减少成功则跳出循环breakelse--6.3未减少成功则value+1再次启动尝试retries=retries+1ifretries>=maxRetriesthen--6.4超越最大尝试次数驳回减少随机数战略localrandom_value=math.random(1,1000)currentTime=currentTime+random_valueelsecurrentTime=currentTime+1endendendreturnfalse
编写AOP阻拦
@AutowiredprivateRedisTemplate<String,Object>redisTemplate;@AutowiredprivateRedisScript<Boolean>limitScript;/***限流*XXX对限流要求比拟高,可以经常使用在Redis中对规则启动存储校验或许经常使用两边件**@paramjoinPointjoinPoint*@paramrateLimiter限流注解*/@Before(value="@annotation(rateLimiter)")publicvoidboBefore(JoinPointjoinPoint,RateLimiterrateLimiter){//1.生成keyStringkey=getCombineKey(rateLimiter,joinPoint);try{//2.口头脚本前往能否限流Booleanflag=redisTemplate.execute(limitScript,ListUtil.of(key,String.valueOf(System.currentTimeMillis())),(Object[])getRules(rateLimiter));//3.判别能否限流if(Boolean.TRUE.equals(flag)){log.error("ip:'{}'阻拦到一个恳求RedisKey:'{}'",IpUtil.getIpAddr(((ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest()),key);thrownewServiceException(rateLimiter.message());}}catch(ServiceExceptione){throwe;}catch(Exceptione){e.printStackTrace();}}/***失掉规则**@paramrateLimiter失掉其中规则信息*@return*/privateLong[]getRules(RateLimiterrateLimiter){intcapacity=rateLimiter.rules().length<<1;//1.构建argsLong[]args=newLong[rateLimiter.preventDuplicate()?capacity+2:capacity];//3.记载数组元素intindex=0;//2.判别能否须要减少防重复提交到redis启动校验if(rateLimiter.preventDuplicate()){RateRulepreventRateRule=rateLimiter.preventDuplicateRule();args[index++]=preventRateRule.count();args[index++]=preventRateRule.timeUnit().toMillis(preventRateRule.time());}RateRule[]rules=rateLimiter.rules();for(RateRulerule:rules){args[index++]=rule.count();args[index++]=rule.timeUnit().toMillis(rule.time());}returnargs;}
以上,欢迎大家提出意见。
关于API网关(四)——限流
流量限制,站在用户或者运营的角度看,最直观能感受到的作用是——收费 各大主流开放平台的对外API,一般都有一些免费的额度,可以供个人测试用,一旦想大规模调用,就需要付费购买更大的额度(频率、次数),根据调用次数或者频率进行收费。一旦超过拥有的额度,就会被限制调用。
其实这才是限流最大的用处,只是用户或者运营同学无感,所以不太被大多数人了解。 网关后面是各个服务,各个服务的接口通过网关透出去给用户调用。理论上说,用户的流量是不可预知的,随时可能来一波,一旦流量的峰值超过了服务的承载能力,服务就挂了,比如有大新闻发生时的某浪微博,比如前些年的. 所以, 网关必须保证,放过去到达后端服务的流量一定不可以超过服务可以承载的上限 。这个上限,是网关和各个服务协商出来的。
由简到难,限流可以 分为单机限流、单集群限流、全集群限流 。 这里不讨论具体的如漏桶、令牌桶等限流算法,只说概念和思想。
单机限流的思想很简单,就是每个机器的限流值 x 机器数量 = 总的限流值。 举个例子,A用户的QPS限制是100,网关部署了10台机器,那么,每台机器限制10QPS就可以了。 先说好处,这种方法实现起来非常简单,每台机器在本地内存计算qps就可以了,超过阈值就拒流。 不过单机限流的缺陷也十分明显,主要体现在两点: 当网关部署的机器数量发生变化时,每台机器的限流值需要根据机器数调整。现实中,因为扩容、缩容、机器宕机等原因,机器数的变化是常有的事。 单机限流的前提是,每台网关承载的用户的流量是平均的,但是事实上,在某些时间,用户的流量并不是完全平均分布在每台机器上的。 举个例子: 10台机器,每台限qps10,其中3台每台实际qps是15,因为超限导致用户流量被拒。其余7台每台qps是7。这样用户总的qps = 15 * 3 + 7 * 7 = 94. 用户qps并没有超限,但是却有一部分流量被拒了,这样就很有问题。 实际上,单台限流的阈值也会设置的稍微大一些,以抵消流量不均的问题。 因为上面的问题, 单机限流通常作为一种兜底的备用手段,大多数时候用的还是集群限流 。
先来看一个示意图:
相比单机限流,集群限流的计数工作上移到redis集群内进行,解决了单机限流的缺陷。 但是集群限流也不是完美的,因为引入了redis,那么,当网关和redis之间的网络抖动、redis本身故障时,集群限流就失效了,这时候,还是得依靠单机限流进行兜底。 也就是说, 集群限流 + 单机限流配合,才是一个比稳妥的方案 。
接下来我们来思考这样一个问题:大型网关一般都是多机房、多地域部署的,当然,后端的服务也是多机房、多地域部署的,在保护服务这一点来说,集群限流是够用了。但是对用户来说,还是有一些问题: 比如,用户购买的QPS上限是30,我们的网关部署在中国北、中、南三个地域,那么这30QPS怎么分配呢? 平均肯定不行,用户的流量可能是明显不均衡的,比如用户的业务主要集中在中国北方,那么用户的流量大部分都会进入北方的网关,网关如果限制QPS为10的话,用户肯定来投诉。 那每个地域都限制为30行不行?也不行,如果用户的流量比较均匀的分布在各个地域,那么用户购买了30QPS,实际上可能使用了90QPS,这太亏了。 按照解决单机限流流量不均的思路,搞一个公共的redis集群来计数行不行? 也不行,受限于信号传播速度和天朝的广阔疆域,每个流量都计数,肯定不现实,rt太高会导致限流失去意义,带宽成本也会变得极其昂贵,对redis的规格要求也会很高。总之,很贵还解决不了问题。 有一种巧妙的解决办法是:本地集群阶梯计数 + 全集群检查。 还是刚才的例子: 限流阈值时90,那么三个地域各自计数,当本地域的数值达到30时,去其他两个地域取一次对方当前的计数值,三个地域的计数值加起来,如果超了,告诉另外两个地域超了,开始拒流。如果没超,本地QPS每上涨10,重复一次上述的动作。 这样就能有效的减少与redis的交互次数,同时实现了全地域真·集群限流。 当然,这种全地域集群限流,因为rt和阶梯计数间隔的存在,一定是不准的,但是,比单集群限流还是好很多。
当某个用户流量特别大的时候,redis计数就会遇到典型的热点key问题,导致redis集群单节点压力过大, 有两种办法可以解决这个问题:打散和抽样。
打散是指,把热点key加一些后缀,使其变成多个key,从而hash到不通的redis节点上,均摊压力。 比如热点key是abcd,那么打散后,key变成了abcd1、abcd2、abcd3、abcd4。技术时,轮流加1、2、3、4的后缀就可以了。
抽样是指,针对热点key,不是每个每个请求到来时都进行计数,而是进行一个抽样,比如每10个请求记一次数,这样redis的压力就会降低到十分之一。
redis+nodejs实现限流的三种方式
1、基于Redis的setnx的操作,给指定的key设置了过期实践。 2、基于Redis的数据结构zset,将请求打造成一个zset数组。 3、基于Redis的令牌桶算法,输出速率大于输入速率,就要限流。
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。