在分布式領(lǐng)域,我們難免會(huì)遇到并發(fā)量突增,對(duì)后端服務(wù)造成高壓力,嚴(yán)重甚至?xí)?dǎo)致系統(tǒng)宕機(jī)。為避免這種問(wèn)題,我們通常會(huì)為接口添加限流、降級(jí)、熔斷等能力,從而使接口更為健壯。JAVA領(lǐng)域常見(jiàn)的開(kāi)源組件有Netflix的hystrix,阿里系開(kāi)源的sentinel等,都是蠻不錯(cuò)的限流熔斷框架。
今天我們就基于redis組件的特性,實(shí)現(xiàn)一個(gè)分布式限流組件,名字就定為shield-ratelimiter。
原理
首先解釋下為何采用Redis作為限流組件的核心。
通俗地講,假設(shè)一個(gè)用戶(用IP判斷)每秒訪問(wèn)某服務(wù)接口的次數(shù)不能超過(guò)10次,那么我們可以在Redis中創(chuàng)建一個(gè)鍵,并設(shè)置鍵的過(guò)期時(shí)間為60秒。
當(dāng)一個(gè)用戶對(duì)此服務(wù)接口發(fā)起一次訪問(wèn)就把鍵值加1,在單位時(shí)間(此處為1s)內(nèi)當(dāng)鍵值增加到10的時(shí)候,就禁止訪問(wèn)服務(wù)接口。PS:在某種場(chǎng)景中添加訪問(wèn)時(shí)間間隔還是很有必要的。我們本次不考慮間隔時(shí)間,只關(guān)注單位時(shí)間內(nèi)的訪問(wèn)次數(shù)。
需求
原理已經(jīng)講過(guò)了,說(shuō)下需求。
1、基于Redis的incr及過(guò)期機(jī)制開(kāi)發(fā) 2、調(diào)用方便,聲明式 3、Spring支持
基于上述需求,我們決定基于注解方式進(jìn)行核心功能開(kāi)發(fā),基于Spring-boot-starter作為基礎(chǔ)環(huán)境,從而能夠很好的適配Spring環(huán)境。
另外,在本次開(kāi)發(fā)中,我們不通過(guò)簡(jiǎn)單的調(diào)用Redis的java類庫(kù)API實(shí)現(xiàn)對(duì)Redis的incr操作。
原因在于,我們要保證整個(gè)限流的操作是原子性的,如果用Java代碼去做操作及判斷,會(huì)有并發(fā)問(wèn)題。這里我決定采用Lua腳本進(jìn)行核心邏輯的定義。
為何使用Lua
在正式開(kāi)發(fā)前,我簡(jiǎn)單介紹下對(duì)Redis的操作中,為何推薦使用Lua腳本。
1、減少網(wǎng)絡(luò)開(kāi)銷: 不使用 Lua 的代碼需要向 Redis 發(fā)送多次請(qǐng)求, 而腳本只需一次即可, 減少網(wǎng)絡(luò)傳輸; 2、原子操作: Redis 將整個(gè)腳本作為一個(gè)原子執(zhí)行, 無(wú)需擔(dān)心并發(fā), 也就無(wú)需事務(wù); 3、復(fù)用: 腳本會(huì)永久保存 Redis 中, 其他客戶端可繼續(xù)使用.
Redis添加了對(duì)Lua的支持,能夠很好的滿足原子性、事務(wù)性的支持,讓我們免去了很多的異常邏輯處理。對(duì)于Lua的語(yǔ)法不是本文的主要內(nèi)容,
正式開(kāi)發(fā)
到這里,我們正式開(kāi)始手寫(xiě)限流組件的進(jìn)程。
1. 工程定義
項(xiàng)目基于maven構(gòu)建,主要依賴Spring-boot-starter,我們主要在springboot上進(jìn)行開(kāi)發(fā),因此自定義的開(kāi)發(fā)包可以直接依賴下面這個(gè)坐標(biāo),方便進(jìn)行包管理。版本號(hào)自行選擇穩(wěn)定版。
2. Redis整合
由于我們是基于Redis進(jìn)行的限流操作,因此需要整合Redis的類庫(kù),上面已經(jīng)講到,我們是基于Springboot進(jìn)行的開(kāi)發(fā),因此這里可以直接整合RedisTemplate。
2.1 坐標(biāo)引入
這里我們引入spring-boot-starter-redis的依賴。
2.2 注入CacheManager及RedisTemplate
新建一個(gè)Redis的配置類,命名為RedisCacheConfig,使用javaconfig形式注入CacheManager及RedisTemplate。為了操作方便,我們采用了Jackson進(jìn)行序列化。代碼如下
注意要使用@Configuration 標(biāo)注此類為一個(gè)配置類,當(dāng)然你可以使用@Component , 但是不推薦,原因在于@Component 注解雖然也可以當(dāng)作配置類,但是并不會(huì)為其生成CGLIB代理Class,而使用@Configuration ,CGLIB會(huì)為其生成代理類,進(jìn)行性能的提升。
2.3 調(diào)用方Application.propertie需要增加Redis配置
我們的包開(kāi)發(fā)完畢之后,調(diào)用方的application.properties需要進(jìn)行相關(guān)配置如下:
如果有密碼的話,配置password即可。
這里為單機(jī)配置,如果需要支持哨兵集群,則配置如下,Java代碼不需要改動(dòng),只需要變動(dòng)配置即可。注意 兩種配置不能共存!
3. 定義注解
為了調(diào)用方便,我們定義一個(gè)名為RateLimiter 的注解,內(nèi)容如下
該注解明確只用于方法,主要有三個(gè)屬性。
1、key–表示限流模塊名,指定該值用于區(qū)分不同應(yīng)用,不同場(chǎng)景,推薦格式為:應(yīng)用名:模塊名:ip:接口名:方法名 2、limit–表示單位時(shí)間允許通過(guò)的請(qǐng)求數(shù) 3、expire–incr的值的過(guò)期時(shí)間,業(yè)務(wù)中表示限流的單位時(shí)間。
4. 解析注解
定義好注解后,需要開(kāi)發(fā)注解使用的切面,這里我們直接使用aspectj進(jìn)行切面的開(kāi)發(fā)。先看代碼
這里是注入了RedisTemplate,使用其API進(jìn)行Lua腳本的調(diào)用。
init() 方法在應(yīng)用啟動(dòng)時(shí)會(huì)初始化DefaultRedisScript,并加載Lua腳本,方便進(jìn)行調(diào)用。
PS: Lua腳本放置在classpath下,通過(guò)ClassPathResource進(jìn)行加載。
這里我們定義了一個(gè)切點(diǎn),表示只要注解了@RateLimiter 的方法,均可以觸發(fā)限流操作。
這段代碼的邏輯為,獲取 @RateLimiter 注解配置的屬性:key、limit、expire,并通過(guò)redisTemplate.execute(RedisScriptscript,Listkeys,Object…args) 方法傳遞給Lua腳本進(jìn)行限流相關(guān)操作,邏輯很清晰。
這里我們定義如果腳本返回狀態(tài)為0則為觸發(fā)限流,1表示正常請(qǐng)求。
5. Lua腳本
這里是我們整個(gè)限流操作的核心,通過(guò)執(zhí)行一個(gè)Lua腳本進(jìn)行限流的操作。腳本內(nèi)容如下
邏輯很通俗,我簡(jiǎn)單介紹下。
1、首先腳本獲取Java代碼中傳遞而來(lái)的要限流的模塊的key,不同的模塊key值一定不能相同,否則會(huì)覆蓋!2、redis.call(‘incr’, key1)對(duì)傳入的key做incr操作,如果key首次生成,設(shè)置超時(shí)時(shí)間ARGV[1];(初始值為1) 3、ttl是為防止某些key在未設(shè)置超時(shí)時(shí)間并長(zhǎng)時(shí)間已經(jīng)存在的情況下做的保護(hù)的判斷;4、每次請(qǐng)求都會(huì)做+1操作,當(dāng)限流的值val大于我們注解的閾值,則返回0表示已經(jīng)超過(guò)請(qǐng)求限制,觸發(fā)限流。否則為正常請(qǐng)求。
當(dāng)過(guò)期后,又是新的一輪循環(huán),整個(gè)過(guò)程是一個(gè)原子性的操作,能夠保證單位時(shí)間不會(huì)超過(guò)我們預(yù)設(shè)的請(qǐng)求閾值。
到這里我們便可以在項(xiàng)目中進(jìn)行測(cè)試。
測(cè)試
這里我貼一下核心代碼,我們定義一個(gè)接口,并注解@RateLimiter(key=“ratedemo:1.0.0”,limit=5,expire=100) 表示模塊ratedemo:sendPayment:1.0.0 在100s內(nèi)允許通過(guò)5個(gè)請(qǐng)求,這里的參數(shù)設(shè)置是為了方便看結(jié)果。實(shí)際中,我們通常會(huì)設(shè)置1s內(nèi)允許通過(guò)的次數(shù)。
我們通過(guò)RestClient請(qǐng)求接口,日志返回如下:
根據(jù)日志能夠看到,正常請(qǐng)求5次后,返回限流觸發(fā),說(shuō)明我們的邏輯生效,對(duì)前端而言也是可以看到false標(biāo)記,表明我們的Lua腳本限流邏輯是正確的,這里具體返回什么標(biāo)記需要調(diào)用方進(jìn)行明確的定義。
總結(jié)
我們通過(guò)Redis的incr及expire功能特性,開(kāi)發(fā)定義了一套基于注解的分布式限流操作,核心邏輯基于Lua保證了原子性。達(dá)到了很好的限流的目的,生產(chǎn)上,可以基于該特點(diǎn)進(jìn)行定制自己的限流組件,當(dāng)然你可以參考本文的代碼,相信你寫(xiě)的一定比我的demo更好!