当前位置: 首页 > 科技观察

我们公司用了6年的Redis分布式限流器,可以说是非常强大了

时间:2023-03-20 23:02:08 科技观察

1.什么是限流?为什么要限流?不知道大家有没有试过在帝都坐地铁,就是那种要排队进地铁站的。为什么要排这么长的队?答案是限流!由于地铁的运力有限,一次挤进去的人太多,会造成站台拥堵,列车超载,存在一定的安全隐患。同理,我们的程序也是一样的。它处理请求的能力也是有限的。一旦请求数超过它的处理限制,它就会崩溃。为了避免最严重的撞车事故,只能拖延大家进站的时间。限流是保证系统高可用的重要手段!!!由于互联网公司流量巨大,系统上线时会进行流量峰值评估,尤其是各种闪购活动。为了保证系统不被庞大的流量压垮,当系统流量达到一定阈值时,会拒绝部分流量。.限流会导致系统在短时间内(这个时间是毫秒级)对用户不可用。一般我们衡量系统处理能力的指标是每秒QPS或者TPS。假设系统每秒的流量阈值为1000,理论上当一秒内第1001个请求进来时,该请求就会被限制。二、限流方案1、计数器Java也可以使用原子类计数器AtomicInteger和Semaphore信号量来做简单的限流。//指定时间内的限流次数privateintmaxCount=10;//privatelonginterval=60;//原子类计数器privateAtomicIntegeratomicInteger=newAtomicInteger(0);//开始时间privatelongstartTime=System.currentTimeMillis();publicbooleanlimit(intmaxCount,intinterval){atomicInteger.addAndGet(1);if(atomicInteger.get()==1){startTime=System.currentTimeMillis();atomicInteger.addAndGet(1);returntrue;}//超过间隔时间,直接重启Countif(System.currentTimeMillis()-startTime>interval*1000){startTime=System.currentTimeMillis();atomicInteger.set(1);returntrue;}//还在间隔时间内,检查是否有超过当前的limitNumberif(atomicInteger.get()>maxCount){returnfalse;}returntrue;}2.漏桶算法漏桶算法的思想很简单。我们把水比作一个请求,把漏桶比作系统处理能力的极限。水先进入漏桶,漏桶中的水以一定的速度流出。当流出速率低于流入速率时,由于漏水桶容量有限,后续的水会直接溢出(拒绝请求),从而实现限流。3.令牌桶算法令牌桶算法的原理也比较简单。我们可以理解为医院看病挂号。拿到号码后,医生才能确诊。系统会维护一个令牌(token)桶,并以恒定的速度向桶中放入令牌(token)。这时候如果有请求进来,想要处理,就需要先从桶中获取一个令牌(token)。),当桶中没有令牌时,请求将被拒绝服务。令牌桶算法通过控制桶的容量和令牌发放速率来限制请求。4.很多Redis+Lua的同学不知道Lua是什么?个人认为,Lua脚本类似于MySQL数据库中的存储过程。它们执行一组命令,所有命令要么成功,要么失败,以实现原子性。Lua脚本也可以理解为带有业务逻辑的代码块。Lua本身就是一种编程语言。虽然redis官方没有直接提供相应的限流API,但是支持Lua脚本的功能。它可用于实现复杂的令牌桶或漏桶算法,这些算法在分布式系统中也有实现。限流的主要方法之一。与Redis事务相比,Lua脚本的优势:降低网络开销:使用Lua脚本,不需要向Redis发送多次请求,只需执行一次,减少网络传输原子操作:Redis将整个Lua脚本作为一个command,atomic,无需担心并发复用:Lua脚本一旦执行,会永久保存在Redis中,其他客户端可以复用Lua脚本。)localkey=KEYS[1]--获取调用脚本时传入的第一个参数值(限流大小)locallimit=tonumber(ARGV[1])--获取当前流量大小localcurentLimit=tonumber(redis.call('get',key)or"0")--是否超过当前限制ifcurentLimit+1>limitthen--返回(拒绝)return0else--不超过value+1redis.call("INCRBY",key,1)--setexpirationTimeredis.call("EXPIRE",key,2)--return(release)return1end通过KEYS[1]获取传入的key参数通过ARGV[1]redis.call方法获取传入的limit参数,从thecache获取key相关的值,如果为null则返回0然后判断缓存中记录的值是否大于限制大小,如果超过则说明当前被限制,返回0如果不是,则将key的缓存值+1,并设置过期时间为1秒后,返回缓存值+1。该方法是本文推荐的方案,具体实现将在后面详细介绍。5.网关层限流限流往往在网关层面进行,如Nginx、Openresty、kong、zuul、SpringCloudGateway等,而像springcloud-gateway这样的网关限流底层实现原理是基于Redis+Lua,通过内置Lua节流脚本的方式。3、Redis+Lua限流的实现接下来我们通过自定义注解、aop、Redis+Lua来实现限流。步骤会更详细。为了让新手快速上手,这里稍微说一下,有经验的老手会照顾。一、环境准备springboot项目创建地址:https://start.spring.io,非常方便实用的工具。2.引入依赖包在pom文件中添加如下依赖包,关键的是spring-boot-starter-data-redis和spring-boot-starter-aop。<依赖项>org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-data-redisorg.springframework.bootspring-boot-starter-aopcom.google.guavaguava21.0org.springframework.bootspring-boot-starter-testorg.apache.commonscommons-lang3<依赖关系><组id>org.springframework.bootspring-boot-starter-testtestorg.junit.vintagejunit-vintage-engine3.在application.properties文件中配置application.properties,配置预建的redis服务地址和端口spring.redis.host=127.0.0.1spring.redis.port=63794,配置RedisTemplate实例@ConfigurationpublicclassRedisLimiterHelper{@BeanpublicRedisTemplatelimitRedisTemplate(LettuceConnectionFactoryredisConnectionFactory){RedisTemplatetemplate=newRedis).setKeySerializer(newString(RedisSerializer)));template.setValueSerializer(newGenericJackson2JsonRedisSerializer());template.setConnectionFactory(redisConnectionFactory);returntemplate;}}限流类型枚举类/***@authorfu*@description限流类型*@date2020/4/813:47*/publicenumLimitType{/***customkey*/CUSTOMER,/***requesterIP*/IP;}5.自定义注解我们自定义一个@Limit注解,注解类型为ElementType。METHOD作用于方法。period表示请求限制时间段,count表示在period周期内允许释放请求的次数。limitType表示限流的类型,可以根据请求的IP和key自定义。如果不传limitType属性,则默认使用方法名作为默认key。/***@authorfu*@description自定义限流注解*@date2020/4/813:15*/@Target({ElementType.METHOD,ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic@interfaceLimit{/***Name*/Stringname()default"";/***key*/Stringkey()default"";/***Keyprefix*/Stringprefix()default"";/***给定时间rangeunit(seconds)*/intperiod();/***一定时间内最大访问次数*/intcount();/***限流类型(用户自定义key或请求ip)*/LimitTypelimitType()defaultLimitType.CUSTOMER;}6.切面代码实现/***@authorfu*@descriptionCurrent限制切面实现*@date2020/4/813:04*/@Aspect@ConfigurationpublicclassLimitInterceptor{privatestaticfinalLoggerlogger=LoggerFactory.getLogger(LimitInterceptor.class);privatestaticfinalStringUNKNOWN="unknown";privatefinalRedisTemplatelimitRedisTemplate;@AutowiredpublicLimitInterceptor(RedisTemplatelimitRedisTemplate){this.limitRedisTemplate=limitRedisTemplate;}/***@parampjp*@authorfu*@description@date2020/4/813:04*/@Around("ex执行(public**(..))&&@annotation(com.xiaofu.limit.api.Limit)")publicObjectinterceptor(ProceedingJoinPointpjp){MethodSignaturesignature=(MethodSignature)pjp.getSignature();Methodmethod=signature.getMethod();LimitlimitAnnotation=method.getAnnotation(Limit.class);LimitTypelimitType=limitAnnotation.limitType();Stringname=limitAnnotation.name();Stringkey;intlimitPeriod=limitAnnotation.period();intlimitCount=limitAnnotation.count();/***根据限制流类型获取不同的key,如果不传我们会以方法命名为key*/switch(limitType){caseIP:key=getIpAddress();break;caseCUSTOMER:key=limitAnnotation.key();break;default:key=StringUtils.upperCase(method.getName());}ImmutableListkeys=ImmutableList.of(StringUtils.join(limitAnnotation.prefix(),key));try{StringluaScript=buildLuaScript();RedisScriptredisScript=newDefaultRedisScript<>(luaScript,Number.class);Numbercount=limitRedisTemplate.execute(redisScript,keys,limitCount,limitPeriod);logger.info("Accessstrycountis{}forname={}andkey={}",count,name,key);if(count!=null&&count.intValue()<=limitCount){returnpjp.proceed();}else{thrownewRuntimeException("Youhavebeendraggedintotheblacklist");}}catch(Throwablee){if(einstanceofRuntimeException){thrownewRuntimeException(e.getLocalizedMessage());}thrownewRuntimeException("serverexception");}}/***@authorfu*@description写redisLua限流脚本*@date2020/4/813:24*/publicStringbuildLuaScript(){StringBuilderlua=newStringBuilder();lua.append("localc");lua.append("\nc=redis.call('get',KEYS[1])");//调用不超过最大值,则直接返回lua.append("\nifcandtonumber(c)>tonumber(ARGV[1])then");lua.append("\nreturnc;");lua.append("\nend");//执行计算器添加lua.append("\nc=redis.call('incr',KEYS[1])");lua.append("\niftonumber(c)==1then");//从第一次调用开始限流,设置对应key值的过期时间lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");lua.append("\nend");lu一个.一个ppend("\nreturnc;");returnlua.toString();}/***@authorfu*@description获取id地址*@date2020/4/813:24*/publicStringgetIpAddress(){HttpServletRequestrequest=((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();Stringip=request.getHeader("x-forwarded-for");if(ip==null||ip.length()==0||UNKNOWN.equalsIgnoreCase(ip)){ip=request.getHeader("Proxy-Client-IP");}if(ip==null||ip.length()==0||UNKNOWN.equalsIgnoreCase(ip)){ip=request.getHeader("WL-Proxy-Client-IP");}if(ip==null||ip.length()==0||UNKNOWN.equalsIgnoreCase(ip)){ip=request.getRemoteAddr();}returnip;}}7、控制层的实现我们在需要限流的接口方法上打上@Limit注解。接下来我们给方法设置@Limit注解,10秒内只允许释放3个请求。这里使用AtomicInteger来计数更直观/***@Author:fu*@Description:*/@RestControllerpublicclassLimiterController{privatestaticfinalAtomicIntegerATOMIC_INTEGER_1=newAtomicInteger();privatestaticfinalAtomicIntegerATOMIC_INTEGER_2=newAtomicInteger();privatestaticfinalAtomicIntegerATOMIC_INTEGER_3=newAtomicInteger();/****@authordate20*@0description813:42*/@Limit(key="limitTest",period=10,count=3)@GetMapping("/limitTest1")publicinttestLimiter1(){returnATOMIC_INTEGER_1.incrementAndGet();}/***@authorfu*@description*@date2020/4/813:42*/@Limit(key="customer_limit_test",period=10,count=3,limitType=LimitType.CUSTOMER)@GetMapping("/limitTest2")publicinttestLimiter2(){returnATOMIC_INTEGER_2.incrementAndGet();}/***@authorfu*@description*@date2020/4/813:42*/@Limit(key="ip_limit_test",period=10,count=3,limitType=LimitType.IP)@GetMapping("/limitTest3")publicinttestLimiter3(){returnATOMIC_INTEGER_3.incrementAndGet();}}8.测试测试“预期”:连续请求3每次都能成功,第四次请求被拒绝。接下来我们看看是不是我们期待的效果。请求地址:http://127.0.0.1:8080/limitTest1,使用postman测试,将postmanurl直接发到浏览器也一样。可以看到,当第四次请求时,应用直接拒绝了请求,说明我们的Springboot+aop+lua限流方案构建成功。上面的springboot+aop+Lua限流的实现比较简单,目的是让大家知道什么是限流?如何做一个简单的限流功能,面试需要知道是什么。虽然上面提到了几种实现限流的方案,具体选择哪一种要结合具体的业务场景,不宜用于实际。

最新推荐
猜你喜欢