我司用了 6 年的 Redis 分布式限流器,很牛逼了!
一、什么是限流?為什么要限流?
不知道大家有沒(méi)有做過(guò)帝都的地鐵,就是進(jìn)地鐵站都要排隊(duì)的那種,為什么要這樣擺長(zhǎng)龍轉(zhuǎn)圈圈?答案就是為了限流!因?yàn)橐惶说罔F的運(yùn)力是有限的,一下擠進(jìn)去太多人會(huì)造成站臺(tái)的擁擠、列車(chē)的超載,存在一定的安全隱患。同理,我們的程序也是一樣,它處理請(qǐng)求的能力也是有限的,一旦請(qǐng)求多到超出它的處理極限就會(huì)崩潰。為了不出現(xiàn)最壞的崩潰情況,只能耽誤一下大家進(jìn)站的時(shí)間。
限流是保證系統(tǒng)高可用的重要手段!!!由于互聯(lián)網(wǎng)公司的流量巨大,系統(tǒng)上線會(huì)做一個(gè)流量峰值的評(píng)估,尤其是像各種秒殺促銷(xiāo)活動(dòng),為了保證系統(tǒng)不被巨大的流量壓垮,會(huì)在系統(tǒng)流量到達(dá)一定閾值時(shí),拒絕掉一部分流量。
限流會(huì)導(dǎo)致用戶在短時(shí)間內(nèi)(這個(gè)時(shí)間段是毫秒級(jí)的)系統(tǒng)不可用,一般我們衡量系統(tǒng)處理能力的指標(biāo)是每秒的QPS或者TPS,假設(shè)系統(tǒng)每秒的流量閾值是1000,理論上一秒內(nèi)有第1001個(gè)請(qǐng)求進(jìn)來(lái)時(shí),那么這個(gè)請(qǐng)求就會(huì)被限流。
二、限流方案
1、計(jì)數(shù)器
Java內(nèi)部也可以通過(guò)原子類計(jì)數(shù)器AtomicInteger、Semaphore信號(hào)量來(lái)做簡(jiǎn)單的限流。
//?限流的個(gè)數(shù)private?int?maxCount?=?10;//?指定的時(shí)間內(nèi)private?long?interval?=?60;//?原子類計(jì)數(shù)器private?AtomicInteger?atomicInteger?=?new?AtomicInteger(0);//?起始時(shí)間private?long?startTime?=?System.currentTimeMillis();public?boolean?limit(int?maxCount,?int?interval)?{atomicInteger.addAndGet(1);if?(atomicInteger.get()?==?1)?{startTime?=?System.currentTimeMillis();atomicInteger.addAndGet(1);return?true;}//?超過(guò)了間隔時(shí)間,直接重新開(kāi)始計(jì)數(shù)if?(System.currentTimeMillis()?-?startTime?>?interval?*?1000)?{startTime?=?System.currentTimeMillis();atomicInteger.set(1);return?true;}//?還在間隔時(shí)間內(nèi),check有沒(méi)有超過(guò)限流的個(gè)數(shù)if?(atomicInteger.get()?>?maxCount)?{return?false;}return?true;}2、漏桶算法
漏桶算法思路很簡(jiǎn)單,我們把水比作是請(qǐng)求,漏桶比作是系統(tǒng)處理能力極限,水先進(jìn)入到漏桶里,漏桶里的水按一定速率流出,當(dāng)流出的速率小于流入的速率時(shí),由于漏桶容量有限,后續(xù)進(jìn)入的水直接溢出(拒絕請(qǐng)求),以此實(shí)現(xiàn)限流。
3、令牌桶算法
令牌桶算法的原理也比較簡(jiǎn)單,我們可以理解成醫(yī)院的掛號(hào)看病,只有拿到號(hào)以后才可以進(jìn)行診病。
系統(tǒng)會(huì)維護(hù)一個(gè)令牌(token)桶,以一個(gè)恒定的速度往桶里放入令牌(token),這時(shí)如果有請(qǐng)求進(jìn)來(lái)想要被處理,則需要先從桶里獲取一個(gè)令牌(token),當(dāng)桶里沒(méi)有令牌(token)可取時(shí),則該請(qǐng)求將被拒絕服務(wù)。令牌桶算法通過(guò)控制桶的容量、發(fā)放令牌的速率,來(lái)達(dá)到對(duì)請(qǐng)求的限制。
4、Redis + Lua
很多同學(xué)不知道Lua是啥?個(gè)人理解,Lua腳本和?MySQL數(shù)據(jù)庫(kù)的存儲(chǔ)過(guò)程比較相似,他們執(zhí)行一組命令,所有命令的執(zhí)行要么全部成功或者失敗,以此達(dá)到原子性。也可以把Lua腳本理解為,一段具有業(yè)務(wù)邏輯的代碼塊。
而Lua本身就是一種編程語(yǔ)言,雖然redis?官方?jīng)]有直接提供限流相應(yīng)的API,但卻支持了?Lua?腳本的功能,可以使用它實(shí)現(xiàn)復(fù)雜的令牌桶或漏桶算法,也是分布式系統(tǒng)中實(shí)現(xiàn)限流的主要方式之一。
相比Redis事務(wù),Lua腳本的優(yōu)點(diǎn):
-
減少網(wǎng)絡(luò)開(kāi)銷(xiāo):使用Lua腳本,無(wú)需向Redis?發(fā)送多次請(qǐng)求,執(zhí)行一次即可,減少網(wǎng)絡(luò)傳輸
-
原子操作:Redis?將整個(gè)Lua腳本作為一個(gè)命令執(zhí)行,原子,無(wú)需擔(dān)心并發(fā)
-
復(fù)用:Lua腳本一旦執(zhí)行,會(huì)永久保存?Redis?中,,其他客戶端可復(fù)用
Lua腳本大致邏輯如下:
--?獲取調(diào)用腳本時(shí)傳入的第一個(gè)key值(用作限流的?key) local?key?=?KEYS[1] --?獲取調(diào)用腳本時(shí)傳入的第一個(gè)參數(shù)值(限流大小) local?limit?=?tonumber(ARGV[1])--?獲取當(dāng)前流量大小 local?curentLimit?=?tonumber(redis.call('get',?key)?or?"0")--?是否超出限流 if?curentLimit?+?1?>?limit?then--?返回(拒絕)return?0 else--?沒(méi)有超出?value?+?1redis.call("INCRBY",?key,?1)--?設(shè)置過(guò)期時(shí)間redis.call("EXPIRE",?key,?2)--?返回(放行)return?1 end-
通過(guò)KEYS[1]?獲取傳入的key參數(shù)
-
通過(guò)ARGV[1]獲取傳入的limit參數(shù)
-
redis.call方法,從緩存中g(shù)et和key相關(guān)的值,如果為null那么就返回0
-
接著判斷緩存中記錄的數(shù)值是否會(huì)大于限制大小,如果超出表示該被限流,返回0
-
如果未超過(guò),那么該key的緩存值+1,并設(shè)置過(guò)期時(shí)間為1秒鐘以后,并返回緩存值+1
這種方式是本文推薦的方案,具體實(shí)現(xiàn)會(huì)在后邊做細(xì)說(shuō)。
5、網(wǎng)關(guān)層限流
限流常在網(wǎng)關(guān)這一層做,比如Nginx、Openresty、kong、zuul、Spring Cloud Gateway等,而像spring cloud - gateway網(wǎng)關(guān)限流底層實(shí)現(xiàn)原理,就是基于Redis + Lua,通過(guò)內(nèi)置Lua限流腳本的方式。
三、Redis + Lua 限流實(shí)現(xiàn)
下面我們通過(guò)自定義注解、aop、Redis + Lua?實(shí)現(xiàn)限流,步驟會(huì)比較詳細(xì),為了小白能讓快速上手這里啰嗦一點(diǎn),有經(jīng)驗(yàn)的老鳥(niǎo)們多擔(dān)待一下。
1、環(huán)境準(zhǔn)備
springboot?項(xiàng)目創(chuàng)建地址:https://start.spring.io,很方便實(shí)用的一個(gè)工具。
2、引入依賴包
pom文件中添加如下依賴包,比較關(guān)鍵的就是?spring-boot-starter-data-redis?和?spring-boot-starter-aop。
???<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>21.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies>3、配置application.properties
在?application.properties?文件中配置提前搭建好的?redis?服務(wù)地址和端口。
spring.redis.host=127.0.0.1spring.redis.port=63794、配置RedisTemplate實(shí)例
@Configuration public?class?RedisLimiterHelper?{@Beanpublic?RedisTemplate<String,?Serializable>?limitRedisTemplate(LettuceConnectionFactory?redisConnectionFactory)?{RedisTemplate<String,?Serializable>?template?=?new?RedisTemplate<>();template.setKeySerializer(new?StringRedisSerializer());template.setValueSerializer(new?GenericJackson2JsonRedisSerializer());template.setConnectionFactory(redisConnectionFactory);return?template;} }限流類型枚舉類
/***?@author?fu*?@description?限流類型*?@date?2020/4/8?13:47*/ public?enum?LimitType?{/***?自定義key*/CUSTOMER,/***?請(qǐng)求者IP*/IP; }5、自定義注解
我們自定義個(gè)@Limit注解,注解類型為ElementType.METHOD即作用于方法上。
period表示請(qǐng)求限制時(shí)間段,count表示在period這個(gè)時(shí)間段內(nèi)允許放行請(qǐng)求的次數(shù)。limitType代表限流的類型,可以根據(jù)請(qǐng)求的IP、自定義key,如果不傳limitType屬性則默認(rèn)用方法名作為默認(rèn)key。
/***?@author?fu*?@description?自定義限流注解*?@date?2020/4/8?13:15*/ @Target({ElementType.METHOD,?ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public?@interface?Limit?{/***?名字*/String?name()?default?"";/***?key*/String?key()?default?"";/***?Key的前綴*/String?prefix()?default?"";/***?給定的時(shí)間范圍?單位(秒)*/int?period();/***?一定時(shí)間內(nèi)最多訪問(wèn)次數(shù)*/int?count();/***?限流的類型(用戶自定義key?或者?請(qǐng)求ip)*/LimitType?limitType()?default?LimitType.CUSTOMER; }6、切面代碼實(shí)現(xiàn)
/***?@author?fu*?@description?限流切面實(shí)現(xiàn)*?@date?2020/4/8?13:04*/ @Aspect @Configuration public?class?LimitInterceptor?{private?static?final?Logger?logger?=?LoggerFactory.getLogger(LimitInterceptor.class);private?static?final?String?UNKNOWN?=?"unknown";private?final?RedisTemplate<String,?Serializable>?limitRedisTemplate;@Autowiredpublic?LimitInterceptor(RedisTemplate<String,?Serializable>?limitRedisTemplate)?{this.limitRedisTemplate?=?limitRedisTemplate;}/***?@param?pjp*?@author?fu*?@description?切面*?@date?2020/4/8?13:04*/@Around("execution(public?*?*(..))?&&?@annotation(com.xiaofu.limit.api.Limit)")public?Object?interceptor(ProceedingJoinPoint?pjp)?{MethodSignature?signature?=?(MethodSignature)?pjp.getSignature();Method?method?=?signature.getMethod();Limit?limitAnnotation?=?method.getAnnotation(Limit.class);LimitType?limitType?=?limitAnnotation.limitType();String?name?=?limitAnnotation.name();String?key;int?limitPeriod?=?limitAnnotation.period();int?limitCount?=?limitAnnotation.count();/***?根據(jù)限流類型獲取不同的key?,如果不傳我們會(huì)以方法名作為key*/switch?(limitType)?{case?IP:key?=?getIpAddress();break;case?CUSTOMER:key?=?limitAnnotation.key();break;default:key?=?StringUtils.upperCase(method.getName());}ImmutableList<String>?keys?=?ImmutableList.of(StringUtils.join(limitAnnotation.prefix(),?key));try?{String?luaScript?=?buildLuaScript();RedisScript<Number>?redisScript?=?new?DefaultRedisScript<>(luaScript,?Number.class);Number?count?=?limitRedisTemplate.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");}}/***?@author?fu*?@description?編寫(xiě)?redis?Lua?限流腳本*?@date?2020/4/8?13:24*/public?String?buildLuaScript()?{StringBuilder?lua?=?new?StringBuilder();lua.append("local?c");lua.append("\nc?=?redis.call('get',KEYS[1])");//?調(diào)用不超過(guò)最大值,則直接返回lua.append("\nif?c?and?tonumber(c)?>?tonumber(ARGV[1])?then");lua.append("\nreturn?c;");lua.append("\nend");//?執(zhí)行計(jì)算器自加lua.append("\nc?=?redis.call('incr',KEYS[1])");lua.append("\nif?tonumber(c)?==?1?then");//?從第一次調(diào)用開(kāi)始限流,設(shè)置對(duì)應(yīng)鍵值的過(guò)期lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");lua.append("\nend");lua.append("\nreturn?c;");return?lua.toString();}/***?@author?fu*?@description?獲取id地址*?@date?2020/4/8?13:24*/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;} }7、控制層實(shí)現(xiàn)
我們將@Limit注解作用在需要進(jìn)行限流的接口方法上,下邊我們給方法設(shè)置@Limit注解,在10秒內(nèi)只允許放行3個(gè)請(qǐng)求,這里為直觀一點(diǎn)用AtomicInteger計(jì)數(shù)。
/***?@Author:?fu*?@Description:*/ @RestController public?class?LimiterController?{private?static?final?AtomicInteger?ATOMIC_INTEGER_1?=?new?AtomicInteger();private?static?final?AtomicInteger?ATOMIC_INTEGER_2?=?new?AtomicInteger();private?static?final?AtomicInteger?ATOMIC_INTEGER_3?=?new?AtomicInteger();/***?@author?fu*?@description*?@date?2020/4/8?13:42*/@Limit(key?=?"limitTest",?period?=?10,?count?=?3)@GetMapping("/limitTest1")public?int?testLimiter1()?{return?ATOMIC_INTEGER_1.incrementAndGet();}/***?@author?fu*?@description*?@date?2020/4/8?13:42*/@Limit(key?=?"customer_limit_test",?period?=?10,?count?=?3,?limitType?=?LimitType.CUSTOMER)@GetMapping("/limitTest2")public?int?testLimiter2()?{return?ATOMIC_INTEGER_2.incrementAndGet();}/***?@author?fu*?@description?*?@date?2020/4/8?13:42*/@Limit(key?=?"ip_limit_test",?period?=?10,?count?=?3,?limitType?=?LimitType.IP)@GetMapping("/limitTest3")public?int?testLimiter3()?{return?ATOMIC_INTEGER_3.incrementAndGet();}}8、測(cè)試
測(cè)試「預(yù)期」:連續(xù)請(qǐng)求3次均可以成功,第4次請(qǐng)求被拒絕。接下來(lái)看一下是不是我們預(yù)期的效果,請(qǐng)求地址:http://127.0.0.1:8080/limitTest1,用postman進(jìn)行測(cè)試,有沒(méi)有postman?url直接貼瀏覽器也是一樣。
可以看到第四次請(qǐng)求時(shí),應(yīng)用直接拒絕了請(qǐng)求,說(shuō)明我們的 Springboot + aop + lua 限流方案搭建成功。
總結(jié)
以上?springboot + aop + Lua?限流實(shí)現(xiàn)是比較簡(jiǎn)單的,旨在讓大家認(rèn)識(shí)下什么是限流?如何做一個(gè)簡(jiǎn)單的限流功能,面試要知道這是個(gè)什么東西。上面雖然說(shuō)了幾種實(shí)現(xiàn)限流的方案,但選哪種還要結(jié)合具體的業(yè)務(wù)場(chǎng)景,不能為了用而用。
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的我司用了 6 年的 Redis 分布式限流器,很牛逼了!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 刷题两个月,从入门到字节offer,这是
- 下一篇: 技术的价值