SpringBootRedis用注釋實現(xiàn)接口限流詳解_第1頁
SpringBootRedis用注釋實現(xiàn)接口限流詳解_第2頁
SpringBootRedis用注釋實現(xiàn)接口限流詳解_第3頁
SpringBootRedis用注釋實現(xiàn)接口限流詳解_第4頁
SpringBootRedis用注釋實現(xiàn)接口限流詳解_第5頁
已閱讀5頁,還剩5頁未讀 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

第SpringBootRedis用注釋實現(xiàn)接口限流詳解目錄1.準備工作2.限流注解3.定制RedisTemplate4.開發(fā)Lua腳本5.注解解析6.接口測試7.全局異常處理

1.準備工作

首先我們創(chuàng)建一個SpringBoot工程,引入Web和Redis依賴,同時考慮到接口限流一般是通過注解來標記,而注解是通過AOP來解析的,所以我們還需要加上AOP的依賴,最終的依賴如下:

dependency

groupIdorg.springframework.boot/groupId

artifactIdspring-boot-starter-data-redis/artifactId

/dependency

dependency

groupIdorg.springframework.boot/groupId

artifactIdspring-boot-starter-web/artifactId

/dependency

dependency

groupIdorg.springframework.boot/groupId

artifactIdspring-boot-starter-aop/artifactId

/dependency

然后提前準備好一個Redis實例,這里我們項目配置好之后,直接配置一下Redis的基本信息即可,如下:

spring.redis.host=localhost

spring.redis.port=6379

spring.redis.password=123

好啦,準備工作就算是到位了。

2.限流注解

接下來我們創(chuàng)建一個限流注解,我們將限流分為兩種情況:

針對當前接口的全局性限流,例如該接口可以在1分鐘內訪問100次。針對某一個IP地址的限流,例如某個IP地址可以在1分鐘內訪問100次。

針對這兩種情況,我們創(chuàng)建一個枚舉類:

publicenumLimitType{

*默認策略全局限流

DEFAULT,

*根據(jù)請求者IP進行限流

}

接下來我們來創(chuàng)建限流注解:

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

@Documented

public@interfaceRateLimiter{

*限流key

Stringkey()default"rate_limit:";

*限流時間,單位秒

inttime()default60;

*限流次數(shù)

intcount()default100;

*限流類型

LimitTypelimitType()defaultLimitType.DEFAULT;

}

第一個參數(shù)限流的key,這個僅僅是一個前綴,將來完整的key是這個前綴再加上接口方法的完整路徑,共同組成限流key,這個key將被存入到Redis中。

另外三個參數(shù)好理解,我就不多說了。

好了,將來哪個接口需要限流,就在哪個接口上添加@RateLimiter注解,然后配置相關參數(shù)即可。

3.定制RedisTemplate

小伙伴們知道,在SpringBoot中,我們其實更習慣使用SpringDataRedis來操作Redis,不過默認的RedisTemplate有一個小坑,就是序列化用的是JdkSerializationRedisSerializer,不知道小伙伴們有沒有注意過,直接用這個序列化工具將來存到Redis上的key和value都會莫名其妙多一些前綴,這就導致你用命令讀取的時候可能會出錯。

例如存儲的時候,key是name,value是javaboy,但是當你在命令行操作的時候,getname卻獲取不到你想要的數(shù)據(jù),原因就是存到redis之后name前面多了一些字符,此時只能繼續(xù)使用RedisTemplate將之讀取出來。

我們用Redis做限流會用到Lua腳本,使用Lua腳本的時候,就會出現(xiàn)上面說的這種情況,所以我們需要修改RedisTemplate的序列化方案。

可能有小伙伴會說為什么不用StringRedisTemplate呢?StringRedisTemplate確實不存在上面所說的問題,但是它能夠存儲的數(shù)據(jù)類型不夠豐富,所以這里不考慮。

修改RedisTemplate序列化方案,代碼如下:

@Configuration

publicclassRedisConfig{

@Bean

publicRedisTemplateObject,ObjectredisTemplate(RedisConnectionFactoryconnectionFactory){

RedisTemplateObject,ObjectredisTemplate=newRedisTemplate();

redisTemplate.setConnectionFactory(connectionFactory);

//使用Jackson2JsonRedisSerialize替換默認序列化(默認采用的是JDK序列化)

Jackson2JsonRedisSerializerObjectjackson2JsonRedisSerializer=newJackson2JsonRedisSerializer(Object.class);

ObjectMapperom=newObjectMapper();

om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);

om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

jackson2JsonRedisSerializer.setObjectMapper(om);

redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);

redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);

redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

returnredisTemplate;

}

這個其實也沒啥好說的,key和value我們都使用SpringBoot中默認的jackson序列化方式來解決。

4.開發(fā)Lua腳本

Redis中的一些原子操作我們可以借助Lua腳本來實現(xiàn),想要調用Lua腳本,我們有兩種不同的思路:

在Redis服務端定義好Lua腳本,然后計算出來一個散列值,在Java代碼中,通過這個散列值鎖定要執(zhí)行哪個Lua腳本。直接在Java代碼中將Lua腳本定義好,然后發(fā)送到Redis服務端去執(zhí)行。

SpringDataRedis中也提供了操作Lua腳本的接口,還是比較方便的,所以我們這里就采用第二種方案。

我們在resources目錄下新建lua文件夾專門用來存放lua腳本,腳本內容如下:

localkey=KEYS[1]

localcount=tonumber(ARGV[1])

localtime=tonumber(ARGV[2])

localcurrent=redis.call(get,key)

ifcurrentandtonumber(current)countthen

returntonumber(current)

end

current=redis.call(incr,key)

iftonumber(current)==1then

redis.call(expire,key,time)

end

returntonumber(current)

這個腳本其實不難,大概瞅一眼就知道干啥用的。KEYS和ARGV都是一會調用時候傳進來的參數(shù),tonumber就是把字符串轉為數(shù)字,redis.call就是執(zhí)行具體的redis指令,具體流程是這樣:

首先獲取到傳進來的key以及限流的count和時間time。通過get獲取到這個key對應的值,這個值就是當前時間窗內這個接口可以訪問多少次。如果是第一次訪問,此時拿到的結果為nil,否則拿到的結果應該是一個數(shù)字,所以接下來就判斷,如果拿到的結果是一個數(shù)字,并且這個數(shù)字還大于count,那就說明已經超過流量限制了,那么直接返回查詢的結果即可。如果拿到的結果為nil,說明是第一次訪問,此時就給當前key自增1,然后設置一個過期時間。最后把自增1后的值返回就可以了。

其實這段Lua腳本很好理解。

接下來我們在一個Bean中來加載這段Lua腳本,如下:

@Bean

publicDefaultRedisScriptLonglimitScript(){

DefaultRedisScriptLongredisScript=newDefaultRedisScript();

redisScript.setScriptSource(newResourceScriptSource(newClassPathResource("lua/limit.lua")));

redisScript.setResultType(Long.class);

returnredisScript;

}

我們的Lua腳本現(xiàn)在就準備好了。

5.注解解析

接下來我們就需要自定義切面,來解析這個注解了,我們來看看切面的定義:

@Aspect

@Component

publicclassRateLimiterAspect{

privatestaticfinalLoggerlog=LoggerFactory.getLogger(RateLimiterAspect.class);

@Autowired

privateRedisTemplateObject,ObjectredisTemplate;

@Autowired

privateRedisScriptLonglimitScript;

@Before("@annotation(rateLimiter)")

publicvoiddoBefore(JoinPointpoint,RateLimiterrateLimiter)throwsThrowable{

Stringkey=rateLimiter.key();

inttime=rateLimiter.time();

intcount=rateLimiter.count();

StringcombineKey=getCombineKey(rateLimiter,point);

ListObjectkeys=Collections.singletonList(combineKey);

try{

Longnumber=redisTemplate.execute(limitScript,keys,count,time);

if(number==null||Value()count){

thrownewServiceException("訪問過于頻繁,請稍候再試");

("限制請求'{}',當前請求'{}',緩存key'{}'",count,Value(),key);

}catch(ServiceExceptione){

throwe;

}catch(Exceptione){

thrownewRuntimeException("服務器限流異常,請稍候再試");

publicStringgetCombineKey(RateLimiterrateLimiter,JoinPointpoint){

StringBufferstringBuffer=newStringBuffer(rateLimiter.key());

if(rateLimiter.limitType()==LimitType.IP){

stringBuffer.append(IpUtils.getIpAddr(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest())).append("-");

MethodSignaturesignature=(MethodSignature)point.getSignature();

Methodmethod=signature.getMethod();

ClasstargetClass=method.getDeclaringClass();

stringBuffer.append(targetClass.getName()).append("-").append(method.getName());

returnstringBuffer.toString();

}

這個切面就是攔截所有加了@RateLimiter注解的方法,在前置通知中對注解進行處理。

首先獲取到注解中的key、time以及count三個參數(shù)。獲取一個組合的key,所謂的組合的key,就是在注解的key屬性基礎上,再加上方法的完整路徑,如果是IP模式的話,就再加上IP地址。以IP模式為例,最終生成的key類似這樣:rate_limit:-org.javaboy.ratelimiter.controller.HelloController-hello(如果不是IP模式,那么生成的key中就不包含IP地址)。將生成的key放到集合中。通過redisTemplate.execute方法取執(zhí)行一個Lua腳本,第一個參數(shù)是腳本所封裝的對象,第二個參數(shù)是key,對應了腳本中的KEYS,后面是可變長度的參數(shù),對應了腳本中的ARGV。將Lua腳本執(zhí)行的結果與count進行比較,如果大于count,就說明過載了,拋異常就行了。

好了,大功告成了。

6.接口

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

最新文檔

評論

0/150

提交評論