基于redis的計(jì)數(shù)器限流算法實(shí)現(xiàn)

前言
昨天我們已經(jīng)預(yù)告了今天的內(nèi)容——實(shí)現(xiàn)計(jì)數(shù)器限流算法,所以今天不需要過(guò)多說(shuō)明,我們直接開始正文。
計(jì)數(shù)器限流算法
關(guān)于計(jì)數(shù)器限流算法的實(shí)現(xiàn)原理,我們昨天已經(jīng)介紹過(guò)了,今天的內(nèi)容算是基于我們昨天所說(shuō)的原理的一種應(yīng)用和實(shí)現(xiàn),當(dāng)然還是有必要說(shuō)下我們的實(shí)現(xiàn)思路的:
在接口內(nèi)部最開始的地方,設(shè)置調(diào)用方的計(jì)數(shù)器(key為調(diào)用方唯一的身份信息),第一次調(diào)用時(shí)將其值設(shè)置為1并放進(jìn)緩存中,同時(shí)緩存設(shè)置過(guò)期時(shí)間,有效期內(nèi)每次調(diào)用計(jì)數(shù)器+1,時(shí)間過(guò)期,緩存會(huì)自動(dòng)刪除。可以把相關(guān)邏輯封裝成自定義注解,搞成通用組件,這樣只需要在需要限速的接口上加上對(duì)應(yīng)的的注解即可,明天我們可以來(lái)實(shí)現(xiàn)下。
創(chuàng)建項(xiàng)目
這里我們直接創(chuàng)建一個(gè)spring boot的web項(xiàng)目,然后引入redis客戶端的依賴:
?<dependency>
?????<groupId>org.springframework.datagroupId>
?????<artifactId>spring-data-redisartifactId>
?????<version>2.3.6.RELEASEversion>
dependency>
<dependency>
????<groupId>redis.clientsgroupId>
????<artifactId>jedisartifactId>
dependency>
redis用的是spring boot的RedisTemplate,當(dāng)然你也可以用其他的,沒(méi)有任何限制,然后是redis客戶端設(shè)置:
spring:
??redis:
????database:?0
????host:?127.0.0.1
????port:?6379
????password:?redis1234567
????#?連接超時(shí)時(shí)間(ms)
????timeout:?5000
????#?高版本springboot中使用jedis或者lettuce
????jedis:
??????pool:
????????#?連接池最大連接數(shù)(負(fù)值表示無(wú)限制)
????????max-active:?8
????????#?連接池最大阻塞等待時(shí)間(負(fù)值無(wú)限制)
????????max-wait:?5000
????????#?最大空閑鏈接數(shù)
????????max-idle:?8
????????#?最小空閑鏈接數(shù)
????????min-idle:?1
redis配置類:
@Configuration
public?class?RedisConfig?{
????private?static?Logger?logger?=?LoggerFactory.getLogger(RedisConfig.class);
????@Value("${spring.redis.host}")
????private?String?host;
????@Value("${spring.redis.password}")
????private?String?password;
????@Value("${spring.redis.port}")
????private?int?port;
????@Value("${spring.redis.database}")
????private?int?database;
????@SuppressWarnings("all")
????@Bean
????public?StringRedisTemplate?redisTemplate(RedisConnectionFactory?factory)?{
????????StringRedisTemplate?template?=?new?StringRedisTemplate(factory);
????????Jackson2JsonRedisSerializer?jackson2JsonRedisSerializer?=?new?Jackson2JsonRedisSerializer(Object.class);
????????ObjectMapper?om?=?new?ObjectMapper();
????????om.setVisibility(PropertyAccessor.ALL,?JsonAutoDetect.Visibility.ANY);
????????om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
????????jackson2JsonRedisSerializer.setObjectMapper(om);
????????RedisSerializer?stringSerializer?=?new?StringRedisSerializer();
????????template.setKeySerializer(stringSerializer);
????????template.setValueSerializer(jackson2JsonRedisSerializer);
????????template.setHashKeySerializer(stringSerializer);
????????template.setHashValueSerializer(jackson2JsonRedisSerializer);
????????template.afterPropertiesSet();
????????return?template;
????}
????@Bean
????public?JedisConnectionFactory?jedisConnectionFactory()?{
????????logger.info("jedisConnectionFactory:初始化了");
????????RedisStandaloneConfiguration?configuration?=?new?RedisStandaloneConfiguration();
????????configuration.setHostName(host);
????????configuration.setPassword(RedisPassword.of(password));
????????configuration.setPort(port);
????????configuration.setDatabase(database);
????????return?new?JedisConnectionFactory(configuration);
????}
}
至此,項(xiàng)目的基本環(huán)境基本上搭建完成,下面開始編寫業(yè)務(wù)代碼。
限流業(yè)務(wù)實(shí)現(xiàn)
為了能夠?qū)崿F(xiàn)業(yè)務(wù)層面的低耦合,同時(shí)也為了便于應(yīng)用到實(shí)際業(yè)務(wù)中,這里我將限流器封裝到攔截器中,然后通過(guò)自定義注解的方式實(shí)現(xiàn)攔截器的業(yè)務(wù)去耦合。
限速注解組件
我的第一步是定義一個(gè)計(jì)數(shù)器限流注解組件:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public?@interface?CounterLimit?{
????/**
?????*?調(diào)用方唯一key的名字
?????*?
?????*?@return
?????*/
????String?name();
????/**
?????*?限制訪問(wèn)次數(shù)
?????*?@return
?????*/
????int?limitTimes();
????/**
?????*?限制時(shí)長(zhǎng),也就是計(jì)數(shù)器的過(guò)期時(shí)間
?????*
?????*?@return
?????*/
????long?timeout();
????/**
?????*?限制時(shí)長(zhǎng)單位
?????*
?????*?@return
?????*/
????TimeUnit?timeUnit();
}
注解包括四個(gè)屬性,name表示調(diào)用方身份唯一性的參數(shù)名,比如userId;limitTimes表示限制訪問(wèn)次數(shù),也就是他在指定時(shí)間內(nèi)可以訪問(wèn)多少次;timeout表示限制訪問(wèn)次數(shù)的有效期,一分鐘還是一個(gè)小時(shí);timeUnit表示限速實(shí)際的單位,秒、分鐘、小時(shí)等。
限速攔截器
沒(méi)做之前,考慮的是通過(guò)切面來(lái)實(shí)現(xiàn),但是今天實(shí)際實(shí)踐的時(shí)候,發(fā)現(xiàn)之前想偏了(竟然會(huì)犯入?yún)⒌图?jí)錯(cuò)誤,說(shuō)明最近輪子造的有點(diǎn)少),最終是通過(guò)攔截器實(shí)現(xiàn)的(忠告:沒(méi)事還是要多造輪子,不然容易手生):
@Component
public?class?CounterLimiterHandlerInterceptor?implements?HandlerInterceptor?{
????@Autowired
????private?RedisTemplate?redisTemplate;
????@Override
????public?boolean?preHandle(HttpServletRequest?request,?HttpServletResponse?response,?Object?handler)?throws?Exception?{
????????if?(handler?instanceof?HandlerMethod)?{
????????????HandlerMethod?handlerMethod?=?(HandlerMethod)?handler;
????????????//?判斷方法是否包含CounterLimit,有這個(gè)注解就需要進(jìn)行限速操作
????????????if?(handlerMethod.hasMethodAnnotation(CounterLimit.class))?{
????????????????CounterLimit?annotation?=?handlerMethod.getMethod().getAnnotation(CounterLimit.class);
????????????????JSONObject?result?=?new?JSONObject();
????????????????String?token?=?request.getParameter(annotation.name());
????????????????response.setContentType("text/json;charset=utf-8");
????????????????result.put("timestamp",?System.currentTimeMillis());
????????????????BoundValueOperations?boundGeoOperations?=?redisTemplate.boundValueOps(token);
????????????????//?如果用戶身份唯一key為空,直接返回錯(cuò)誤
????????????????if?(StringUtils.isEmpty(token))?{
????????????????????result.put("result",?"token?is?invalid");
????????????????????response.getWriter().print(JSON.toJSONString(result));
????????????????//?如果限速校驗(yàn)通過(guò),則將請(qǐng)求放行
????????????????}?else?if?(checkLimiter(token,?annotation))?{
????????????????????result.put("result",?"請(qǐng)求成功");
????????????????????Long?expire?=?boundGeoOperations.getExpire();
????????????????????logger.info("result:{}, expire:?{}",??result,?expire);
????????????????????return?true;
????????????????//?否則告知調(diào)用方達(dá)到限速上線
????????????????}?else?{
????????????????????result.put("result",?"達(dá)到訪問(wèn)次數(shù)限制,禁止訪問(wèn)");
????????????????????Long?expire?=?boundGeoOperations.getExpire();
????????????????????logger.info("result:{}, expire:?{}",??result,?expire);
????????????????????response.getWriter().print(JSON.toJSONString(result));
????????????????}
????????????????return?false;
????????????}
????????}
????????return?true;
????}
????/**
????*?限速校驗(yàn)
????*/
????private?Boolean?checkLimiter(String?token,?CounterLimit?annotation)?{
????????BoundValueOperations?boundGeoOperations?=?redisTemplate.boundValueOps(token);
????????Integer?count?=?boundGeoOperations.get();
????????if?(Objects.isNull(count))?{
????????????redisTemplate.boundValueOps(token).set(1,?annotation.timeout(),?annotation.timeUnit());
????????}?else?if?(count?>=?annotation.limitTimes())?{
????????????return?Boolean.FALSE;
????????}?else?{
????????????redisTemplate.boundValueOps(token).set(count?+?1,?boundGeoOperations.getExpire(),?annotation.timeUnit());
????????}
????????return?Boolean.TRUE;
????}
}
代碼邏輯也比較簡(jiǎn)單:
首先判斷接口方法是否包含 CounterLimit注解,有這個(gè)注解就需要進(jìn)行限速操作如果用戶身份唯一 key為空,直接返回錯(cuò)誤如果限速校驗(yàn)通過(guò),則將請(qǐng)求放行,否則告知調(diào)用方達(dá)到限速上線 在校驗(yàn)限速方法中,如果 count為空,表示首次訪問(wèn),則存放一個(gè)count,并設(shè)置過(guò)期時(shí)間如果達(dá)到訪問(wèn)限制上限,直接拒絕,未達(dá)到則 count+1,過(guò)期時(shí)間設(shè)置為剩余時(shí)間
代碼也有比較詳細(xì)的注解,各位小伙伴也應(yīng)該能看懂。
注意: 當(dāng)然如果你的項(xiàng)目本身已經(jīng)有了完善的全局異常處理機(jī)制,這里的攔截器可以直接拋出對(duì)應(yīng)的異常,這里為了方便我偷了個(gè)懶,并沒(méi)有做全局異常處理,而是直接通過(guò)response返回了異常信息,實(shí)際項(xiàng)目開發(fā)中,這種寫法肯定是不合理的,各位小伙伴一定要注意哦!
攔截器配置
這一塊就屬于復(fù)習(xí)內(nèi)容了,也屬于比較入門級(jí)別的spring boot操作了,這里不再過(guò)多贅述,詳細(xì)代碼如下:
@Configuration
public?class?WebConfig?implements?WebMvcConfigurer?{
????@Autowired
????private?CounterLimiterHandlerInterceptor?counterLimiterHandlerInterceptor;
????@Override
????public?void?addInterceptors(InterceptorRegistry?registry)?{
????????//?計(jì)數(shù)器限速
????????registry.addInterceptor(counterLimiterHandlerInterceptor).addPathPatterns("/**");
????????WebMvcConfigurer.super.addInterceptors(registry);
????}
}
接口配置
接口這塊也比較簡(jiǎn)單,就是簡(jiǎn)單的controller方法,然后方法上多了我們的自定義限速器注解CounterLimit,這個(gè)注解的參數(shù)我們上面已經(jīng)解釋過(guò)了,所以這里也就不再贅述:
?@CounterLimit(name?=?"token",limitTimes?=?5,?timeout?=?60,?timeUnit?=?TimeUnit.SECONDS)
????@GetMapping("/limit/count-test")
????public?Object?counterLimiter(String?name,?String?token)?{
????????JSONObject?result?=?new?JSONObject();
???????result.put("data",?"success");
????????return?result;
????}
測(cè)試
完成以上內(nèi)容之后,我們就可以進(jìn)行相關(guān)測(cè)試了,首先將我們的項(xiàng)目啟動(dòng)起來(lái),然后直接訪問(wèn)我們的接口即可,訪問(wèn)接口的時(shí)候記得帶著我們的token(唯一key),最終訪問(wèn)結(jié)果如下:

從結(jié)果中我們可以看出來(lái),在第一次訪問(wèn)的時(shí)候,token的過(guò)期時(shí)間為60,我們連續(xù)訪問(wèn)5次之后,接口限制我們?cè)L問(wèn)的,然后等到限制過(guò)期之后(token過(guò)期),又可以繼續(xù)訪問(wèn)了。至此,我們的計(jì)數(shù)器限流的算法實(shí)現(xiàn)也算是完美達(dá)成,是不是很簡(jiǎn)單呢?
總結(jié)
本次demo總體來(lái)說(shuō)很簡(jiǎn)單,除了算法本身之外,基本上都是java或者spring boot的簡(jiǎn)單知識(shí)點(diǎn)應(yīng)用,但是從我自己實(shí)踐的感受來(lái)說(shuō),我覺(jué)得以后還是得多造輪子,因?yàn)橹氨容^熟悉的好多配置和寫法都生疏了,好多都要翻看之前的demo才能想起來(lái)。當(dāng)然,話句話說(shuō)就是,很多看起來(lái)很簡(jiǎn)單的實(shí)例或者demo,其實(shí)在真正實(shí)踐的時(shí)候并不簡(jiǎn)單,因?yàn)槲覀兺倳?huì)高估自己的能力……

項(xiàng)目完整代碼:
https://github.com/Syske/learning-dome-code/tree/dev/spring-boot-counter-limiter
好了,各位小伙伴,晚安吧!
- END -