当前位置:首页 > 数码 > 成功多规则限流的思索与通常-Redis (成功的规则)

成功多规则限流的思索与通常-Redis (成功的规则)

admin8个月前 (04-15)数码60
市面上很多引见如何成功限流的,然而大局部都有一个缺陷,就是只能成功繁多的限流,比如1分钟访问1次或许60分钟访问10次这种,然而假构想一个接口两种规则都须要满足呢,咱们的名目又是散布式名目,应该如何处置,上方就引见一下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值

Redis

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的令牌桶算法,输出速率大于输入速率,就要限流。

免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。

标签: Redis

“成功多规则限流的思索与通常-Redis (成功的规则)” 的相关文章

Redis数据删除后-内存占用为何居高不下 (redis数据类型)

Redis数据删除后-内存占用为何居高不下 (redis数据类型)

作为面试经验丰富的开发人员,肯定会遇到过这样的问题:做了数据删除操作,为什么使用 top 命令时,仍然显示 Redis 占用了大量内存? 答案 这是因为当数据删除后,Re...

安装并配置-Kubernetes-集群 (安装并配置mysql的实验总结)

安装并配置-Kubernetes-集群 (安装并配置mysql的实验总结)

日常开发中,基于自然支持散布式锁,大家在线上散布式名目中都经常使用过Redis锁。本文重要针对日常开发中加锁环节中某些意外场景启动解说与剖析。本文解说示例代码都在名目目录下 RedisLo...

Redis中分布式锁的防死锁机制 (redis中文网)

Redis中分布式锁的防死锁机制 (redis中文网)

分布式锁在分布式系统中是一种常见的需求。它用于防止对共享资源的并发访问,确保数据一致性和可靠性。在 Redis 中实现分布式锁可以通过使用 SETNX(SET if Not eXists)命令来尝...

排查与处置指南-Redis大Key危害 (排查与处置指的是什么)

排查与处置指南-Redis大Key危害 (排查与处置指的是什么)

这是一位同窗往年秋招参与得物一面遇到的疑问,完整面经如下: 这个疑问在面试中还是比拟容易遇到的,尤其是在调查性能优化相关常识点的时刻。 通常状况下,问了bigkey(大Key)还会继续...

Redis-使用哈希槽而非一致性哈希的原因 (redis淘汰策略有哪些)

Redis-使用哈希槽而非一致性哈希的原因 (redis淘汰策略有哪些)

引言 在分布式系统中,数据分片和负载均衡是至关重要的。哈希槽和一致性哈希两种方法都可以实现这些目标,但它们各有优缺点。本文将讨论为什么在某些情况下使用哈希槽而不是一致性哈希。 哈希槽 哈希...

引发业界震荡-Redis叛逃开源-得罪了 (引发业界震荡的因素)

引发业界震荡-Redis叛逃开源-得罪了 (引发业界震荡的因素)

Redis 许可变更:开源界的巨石 导言 近来,内存数据库供应商 Redis 宣布了一项重大变更,该变更将在开源界掀起轩然大波。Redis 将转向双许可模式,并采用更严格的许可条款。此举引起了社区...

解锁你的海量数据摸索之旅-Redis-全文搜查-的弱小新配置 (解锁海量任务)

解锁你的海量数据摸索之旅-Redis-全文搜查-的弱小新配置 (解锁海量任务)

在2021年我就了解到earch这个名目,并曾经把它用于我的开源名目newbee-mall-pro中。 就我的经常使用体验来说,便捷场景下,用来平替Elasticsearch的经常使用场景曾...