SpringBoot 2.x 優(yōu)雅解決分布式限流
閱讀本文大概需要 9?分鐘。
來自:blog.csdn.net/johnf_nash/article/details/89791808
一、常用的限流算法
1.計數(shù)器方式(傳統(tǒng)計數(shù)器缺點:臨界問題 可能違背定義固定速率原則)

2.令牌桶方式

3、漏桶算法

令牌桶里面裝載的是令牌,然后讓令牌去關(guān)聯(lián)到數(shù)據(jù)發(fā)送,常規(guī)漏桶里面裝載的是數(shù)據(jù),令牌桶允許用戶的正常的持續(xù)突發(fā)量(Bc),就是一次就將桶里的令牌全部用盡的方式來支持續(xù)突發(fā),而常規(guī)的漏桶則不允許用戶任何突發(fā)行。
二、限流實現(xiàn)
基于 redis 的分布式限流
導入依賴
<dependencies>
????
????<dependency>
????????<groupId>org.springframework.bootgroupId>
????????<artifactId>spring-boot-starter-aopartifactId>
????dependency>
????<dependency>
????????<groupId>org.springframework.bootgroupId>
????????<artifactId>spring-boot-starter-webartifactId>
????dependency>
????<dependency>
????????<groupId>org.springframework.bootgroupId>
????????<artifactId>spring-boot-starter-data-redisartifactId>
????dependency>
????<dependency>
????????<groupId>com.google.guavagroupId>
????????<artifactId>guavaartifactId>
????????<version>21.0version>
????dependency>
????<dependency>
????????<groupId>org.apache.commonsgroupId>
????????<artifactId>commons-lang3artifactId>
????dependency>
????<dependency>
????????<groupId>org.springframework.bootgroupId>
????????<artifactId>spring-boot-starter-testartifactId>
????dependency>
dependencies>
屬性配置
application.properites?資源文件中添加 redis 相關(guān)的配置項spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=battcn
Limit 注解
package?com.johnfnash.learn.springboot.ratelimiter.annotation;
import?java.lang.annotation.Documented;
import?java.lang.annotation.ElementType;
import?java.lang.annotation.Inherited;
import?java.lang.annotation.Retention;
import?java.lang.annotation.RetentionPolicy;
import?java.lang.annotation.Target;
//?限流
@Target({ElementType.METHOD,?ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public?@interface?Limit?{
????/**
?????*?資源的名稱
?????*?@return
?????*/
????String?name()?default?"";
????
????/**
?????*?資源的key
?????*
?????*?@return
?????*/
????String?key()?default?"";
????/**
?????*?Key的prefix
?????*
?????*?@return
?????*/
????String?prefix()?default?"";
????
????/**
?????*?給定的時間段
?????*?單位秒
?????*
?????*?@return
?????*/
????int?period();
????/**
?????*?最多的訪問限制次數(shù)
?????*
?????*?@return
?????*/
????int?count();
????
????/**
?????*?類型
?????*
?????*?@return
?????*/
????LimitType?limitType()?default?LimitType.CUSTOMER;
}
package?com.johnfnash.learn.springboot.ratelimiter.annotation;
//?限制的類型
public?enum?LimitType?{
????/**
?????*?自定義key
?????*/
????CUSTOMER,
????/**
?????*?根據(jù)請求者IP
?????*/
????IP;
????
}
RedisTemplate
package?com.johnfnash.learn.springboot.ratelimiter;
import?java.io.Serializable;
import?org.springframework.context.annotation.Bean;
import?org.springframework.context.annotation.Configuration;
import?org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import?org.springframework.data.redis.core.RedisTemplate;
import?org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import?org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public?class?RedisLimiterHelper?{
????@Bean
????public?RedisTemplate?limitRedisTemplate(LettuceConnectionFactory?factory)? {
????????RedisTemplate?template?=?new?RedisTemplate ();
????????template.setKeySerializer(new?StringRedisSerializer());
????????template.setValueSerializer(new?GenericJackson2JsonRedisSerializer());
????????template.setConnectionFactory(factory);
????????return?template;
????}
????
}
Limit 攔截器(AOP)
package?com.johnfnash.learn.springboot.ratelimiter.aop;
import?java.io.Serializable;
import?java.lang.reflect.Method;
import?javax.servlet.http.HttpServletRequest;
import?org.apache.commons.lang3.StringUtils;
import?org.aspectj.lang.ProceedingJoinPoint;
import?org.aspectj.lang.annotation.Around;
import?org.aspectj.lang.annotation.Aspect;
import?org.aspectj.lang.reflect.MethodSignature;
import?org.slf4j.Logger;
import?org.slf4j.LoggerFactory;
import?org.springframework.beans.factory.annotation.Autowired;
import?org.springframework.context.annotation.Configuration;
import?org.springframework.data.redis.core.RedisTemplate;
import?org.springframework.data.redis.core.script.DefaultRedisScript;
import?org.springframework.data.redis.core.script.RedisScript;
import?org.springframework.web.context.request.RequestContextHolder;
import?org.springframework.web.context.request.ServletRequestAttributes;
import?com.google.common.collect.ImmutableList;
import?com.johnfnash.learn.springboot.ratelimiter.annotation.Limit;
import?com.johnfnash.learn.springboot.ratelimiter.annotation.LimitType;
@Aspect
@Configuration
public?class?LimitInterceptor?{
????private?static?final?Logger?logger?=?LoggerFactory.getLogger(LimitInterceptor.class);;
????
????private?final?String?REDIS_SCRIPT?=?buildLuaScript();
????
????@Autowired
????private?RedisTemplate?redisTemplate;
????
????@Around("execution(public?*?*(..))?&&?@annotation(com.johnfnash.learn.springboot.ratelimiter.annotation.Limit)")
????public?Object?interceptor(ProceedingJoinPoint?pjp)?{
????????MethodSignature?signature?=?(MethodSignature)?pjp.getSignature();
????????Method?method?=?signature.getMethod();
????????Limit?limitAnno?=?method.getAnnotation(Limit.class);
????????LimitType?limitType?=?limitAnno.limitType();
????????String?name?=?limitAnno.name();
????????
????????String?key?=?null;
????????int?limitPeriod?=?limitAnno.period();
????????int?limitCount?=?limitAnno.count();
????????switch?(limitType)?{
????????case?IP:
????????????key?=?getIpAddress();
????????????break;
????????case?CUSTOMER:
????????????// TODO 如果此處想根據(jù)表達式或者一些規(guī)則生成?請看?一起來學Spring Boot |?第二十三篇:輕松搞定重復提交(分布式鎖)
????????????key?=?limitAnno.key();
????????????break;
????????default:
????????????break;
????????}
????????
????????ImmutableList?keys?=?ImmutableList.of(StringUtils.join(limitAnno.prefix(),?key));
????????try?{
????????????RedisScript?redisScript?=?new?DefaultRedisScript (REDIS_SCRIPT,?Number.class);
????????????Number?count?=?redisTemplate.execute(redisScript,?keys,?limitCount,?limitPeriod);
????????????logger.info("Access?try?count?is?{}?for?name={}?and?key?=?{}",?count,?name,?key);
????????????if(count?!=?null?&&?count.intValue()?<=?limitCount)?{
????????????????return?pjp.proceed();
????????????}?else?{
????????????????throw?new?RuntimeException("You?have?been?dragged?into?the?blacklist");
????????????}
????????}?catch?(Throwable?e)?{
????????????if?(e?instanceof?RuntimeException)?{
????????????????throw?new?RuntimeException(e.getLocalizedMessage());
????????????}
????????????throw?new?RuntimeException("server?exception");
????????}
????}
????
????/**
?????*?限流?腳本
?????*
?????*?@return?lua腳本
?????*/
????private?String?buildLuaScript()?{
????????StringBuilder?lua?=?new?StringBuilder();
????????lua.append("local?c")
???????????.append("\nc?=?redis.call('get',?KEYS[1])")
???????????//?調(diào)用不超過最大值,則直接返回
???????????.append("\nif?c?and?tonumber(c)?>?tonumber(ARGV[1])?then")
???????????.append("\nreturn?c;")
???????????.append("\nend")
???????????//?執(zhí)行計算器自加
???????????.append("\nc?=?redis.call('incr',?KEYS[1])")
???????????.append("\nif?tonumber(c)?==?1?then")
???????????//?從第一次調(diào)用開始限流,設(shè)置對應鍵值的過期
???????????.append("\nredis.call('expire',?KEYS[1],?ARGV[2])")
???????????.append("\nend")
???????????.append("\nreturn?c;");
????????return?lua.toString();
????}
????
????private?static?final?String?UNKNOWN?=?"unknown";
????public?String?getIpAddress()?{
????????HttpServletRequest?request?=?((ServletRequestAttributes)?RequestContextHolder.getRequestAttributes()).getRequest();
????????String?ip?=?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();
????????}
????????return?ip;
????}
????
}
控制層
@Limit()注解,如下代碼會在 Redis 中生成過期時間為 100s 的?key = test?的記錄,特意定義了一個 AtomicInteger 用作測試…package?com.johnfnash.learn.springboot.ratelimiter.controller;
import?java.util.concurrent.atomic.AtomicInteger;
import?org.springframework.web.bind.annotation.GetMapping;
import?org.springframework.web.bind.annotation.RestController;
import?com.johnfnash.learn.springboot.ratelimiter.annotation.Limit;
@RestController
public?class?LimiterController?{
????private?static?final?AtomicInteger?ATOMIC_INTEGER?=?new?AtomicInteger();
????@Limit(key?=?"test",?period?=?100,?count?=?10)
????//?意味著?100S?內(nèi)最多允許訪問10次
????@GetMapping("/test")
????public?int?testLimiter()?{
????????return?ATOMIC_INTEGER.incrementAndGet();
????}
????
}
測試


總結(jié)
https://github.com/battcn/spring-boot2-learning/tree/master/chapter27
/**
?*?限流?腳本(處理臨界時間大量請求的情況)
?*
?*?@return?lua腳本
?*/
private?String?buildLuaScript2()?{
????StringBuilder?lua?=?new?StringBuilder();
????lua.append("local?listLen,?time")
???????.append("\nlistLen?=?redis.call('LLEN',?KEYS[1])")
???????//?不超過最大值,則直接寫入時間
???????.append("\nif?listLen?and?tonumber(listLen)?)
????????????.append("\nlocal?a?=?redis.call('TIME');")
????????????.append("\nredis.call('LPUSH',?KEYS[1],?a[1]*1000000+a[2])")
???????.append("\nelse")
???????????//?取出現(xiàn)存的最早的那個時間,和當前時間比較,看是小于時間間隔
???????????.append("\ntime?=?redis.call('LINDEX',?KEYS[1],?-1)")
???????????.append("\nlocal?a?=?redis.call('TIME');")
???????????.append("\nif?a[1]*1000000+a[2]?-?time?)
???????????????//?訪問頻率超過了限制,返回0表示失敗
???????????????.append("\nreturn?0;")
???????????.append("\nelse")???????????????????
???????????????.append("\nredis.call('LPUSH',?KEYS[1],?a[1]*1000000+a[2])")
???????????????.append("\nredis.call('LTRIM',?KEYS[1],?0,?tonumber(ARGV[1])-1)")
???????????.append("\nend")???
???????.append("\nend")
???????.append("\nreturn?1;");
????return?lua.toString();
}
if(count?!=?null?&&?count.intValue()?==?1)?{
????return?pjp.proceed();
}?else?{
????throw?new?RuntimeException("You?have?been?dragged?into?the?blacklist");
}
buildLuaScript2()?中的lua腳本,報錯Write commands not allowed after non deterministic commands.https://yq.aliyun.com/articles/195914
redis.replicate_commands()來放開限制。redis.replicate_commands();”,錯誤得以解決。推薦閱讀:
字節(jié)工程師薪資排世界第五,中位數(shù) 43 萬美元,2021 全球程序員收入報告出爐!
內(nèi)容包含Java基礎(chǔ)、JavaWeb、MySQL性能優(yōu)化、JVM、鎖、百萬并發(fā)、消息隊列、高性能緩存、反射、Spring全家桶原理、微服務、Zookeeper、數(shù)據(jù)結(jié)構(gòu)、限流熔斷降級......等技術(shù)棧!
?戳閱讀原文領(lǐng)?。?/span>? ? ? ? ? ? ? ??? ??? ? ? ? ? ? ? ? ? ?朕已閱?
評論
圖片
表情

