'百度架構師分享源碼實戰篇:通過分佈式系統解決限流問題'

Redis Lua 腳本語言 Java 百度 IT技術分享 2019-08-29
"
\t來源:http://dwz.win/dmj

在分佈式領域,我們難免會遇到併發量突增,對後端服務造成高壓力,嚴重甚至會導致系統宕機。為避免這種問題,我們通常會為接口添加限流、降級、熔斷等能力,從而使接口更為健壯。Java領域常見的開源組件有Netflix的hystrix,阿里系開源的sentinel等,都是蠻不錯的限流熔斷框架。

今天我們就基於Redis組件的特性,實現一個分佈式限流組件,名字就定為shield-ratelimiter。

"
\t來源:http://dwz.win/dmj

在分佈式領域,我們難免會遇到併發量突增,對後端服務造成高壓力,嚴重甚至會導致系統宕機。為避免這種問題,我們通常會為接口添加限流、降級、熔斷等能力,從而使接口更為健壯。Java領域常見的開源組件有Netflix的hystrix,阿里系開源的sentinel等,都是蠻不錯的限流熔斷框架。

今天我們就基於Redis組件的特性,實現一個分佈式限流組件,名字就定為shield-ratelimiter。

百度架構師分享源碼實戰篇:通過分佈式系統解決限流問題

原理

首先解釋下為何採用Redis作為限流組件的核心。

通俗地講,假設一個用戶(用IP判斷)每秒訪問某服務接口的次數不能超過10次,那麼我們可以在Redis中創建一個鍵,並設置鍵的過期時間為60秒。

當一個用戶對此服務接口發起一次訪問就把鍵值加1,在單位時間(此處為1s)內當鍵值增加到10的時候,就禁止訪問服務接口。PS:在某種場景中添加訪問時間間隔還是很有必要的。我們本次不考慮間隔時間,只關注單位時間內的訪問次數。

需求

原理已經講過了,說下需求。

1、基於Redis的incr及過期機制開發 2、調用方便,聲明式 3、Spring支持

基於上述需求,我們決定基於註解方式進行核心功能開發,基於Spring-boot-starter作為基礎環境,從而能夠很好的適配Spring環境。

另外,在本次開發中,我們不通過簡單的調用Redis的java類庫API實現對Redis的incr操作。

原因在於,我們要保證整個限流的操作是原子性的,如果用Java代碼去做操作及判斷,會有併發問題。這裡我決定採用Lua腳本進行核心邏輯的定義。

"
\t來源:http://dwz.win/dmj

在分佈式領域,我們難免會遇到併發量突增,對後端服務造成高壓力,嚴重甚至會導致系統宕機。為避免這種問題,我們通常會為接口添加限流、降級、熔斷等能力,從而使接口更為健壯。Java領域常見的開源組件有Netflix的hystrix,阿里系開源的sentinel等,都是蠻不錯的限流熔斷框架。

今天我們就基於Redis組件的特性,實現一個分佈式限流組件,名字就定為shield-ratelimiter。

百度架構師分享源碼實戰篇:通過分佈式系統解決限流問題

原理

首先解釋下為何採用Redis作為限流組件的核心。

通俗地講,假設一個用戶(用IP判斷)每秒訪問某服務接口的次數不能超過10次,那麼我們可以在Redis中創建一個鍵,並設置鍵的過期時間為60秒。

當一個用戶對此服務接口發起一次訪問就把鍵值加1,在單位時間(此處為1s)內當鍵值增加到10的時候,就禁止訪問服務接口。PS:在某種場景中添加訪問時間間隔還是很有必要的。我們本次不考慮間隔時間,只關注單位時間內的訪問次數。

需求

原理已經講過了,說下需求。

1、基於Redis的incr及過期機制開發 2、調用方便,聲明式 3、Spring支持

基於上述需求,我們決定基於註解方式進行核心功能開發,基於Spring-boot-starter作為基礎環境,從而能夠很好的適配Spring環境。

另外,在本次開發中,我們不通過簡單的調用Redis的java類庫API實現對Redis的incr操作。

原因在於,我們要保證整個限流的操作是原子性的,如果用Java代碼去做操作及判斷,會有併發問題。這裡我決定採用Lua腳本進行核心邏輯的定義。

百度架構師分享源碼實戰篇:通過分佈式系統解決限流問題

為何使用Lua

在正式開發前,我簡單介紹下對Redis的操作中,為何推薦使用Lua腳本。

1、減少網絡開銷: 不使用 Lua 的代碼需要向 Redis 發送多次請求, 而腳本只需一次即可, 減少網絡傳輸; 2、原子操作: Redis 將整個腳本作為一個原子執行, 無需擔心併發, 也就無需事務; 3、複用: 腳本會永久保存 Redis 中, 其他客戶端可繼續使用.

Redis添加了對Lua的支持,能夠很好的滿足原子性、事務性的支持,讓我們免去了很多的異常邏輯處理。對於Lua的語法不是本文的主要內容,

正式開發

到這裡,我們正式開始手寫限流組件的進程。

1. 工程定義

項目基於maven構建,主要依賴Spring-boot-starter,我們主要在springboot上進行開發,因此自定義的開發包可以直接依賴下面這個座標,方便進行包管理。版本號自行選擇穩定版。

2. Redis整合

由於我們是基於Redis進行的限流操作,因此需要整合Redis的類庫,上面已經講到,我們是基於Springboot進行的開發,因此這裡可以直接整合RedisTemplate。

"
\t來源:http://dwz.win/dmj

在分佈式領域,我們難免會遇到併發量突增,對後端服務造成高壓力,嚴重甚至會導致系統宕機。為避免這種問題,我們通常會為接口添加限流、降級、熔斷等能力,從而使接口更為健壯。Java領域常見的開源組件有Netflix的hystrix,阿里系開源的sentinel等,都是蠻不錯的限流熔斷框架。

今天我們就基於Redis組件的特性,實現一個分佈式限流組件,名字就定為shield-ratelimiter。

百度架構師分享源碼實戰篇:通過分佈式系統解決限流問題

原理

首先解釋下為何採用Redis作為限流組件的核心。

通俗地講,假設一個用戶(用IP判斷)每秒訪問某服務接口的次數不能超過10次,那麼我們可以在Redis中創建一個鍵,並設置鍵的過期時間為60秒。

當一個用戶對此服務接口發起一次訪問就把鍵值加1,在單位時間(此處為1s)內當鍵值增加到10的時候,就禁止訪問服務接口。PS:在某種場景中添加訪問時間間隔還是很有必要的。我們本次不考慮間隔時間,只關注單位時間內的訪問次數。

需求

原理已經講過了,說下需求。

1、基於Redis的incr及過期機制開發 2、調用方便,聲明式 3、Spring支持

基於上述需求,我們決定基於註解方式進行核心功能開發,基於Spring-boot-starter作為基礎環境,從而能夠很好的適配Spring環境。

另外,在本次開發中,我們不通過簡單的調用Redis的java類庫API實現對Redis的incr操作。

原因在於,我們要保證整個限流的操作是原子性的,如果用Java代碼去做操作及判斷,會有併發問題。這裡我決定採用Lua腳本進行核心邏輯的定義。

百度架構師分享源碼實戰篇:通過分佈式系統解決限流問題

為何使用Lua

在正式開發前,我簡單介紹下對Redis的操作中,為何推薦使用Lua腳本。

1、減少網絡開銷: 不使用 Lua 的代碼需要向 Redis 發送多次請求, 而腳本只需一次即可, 減少網絡傳輸; 2、原子操作: Redis 將整個腳本作為一個原子執行, 無需擔心併發, 也就無需事務; 3、複用: 腳本會永久保存 Redis 中, 其他客戶端可繼續使用.

Redis添加了對Lua的支持,能夠很好的滿足原子性、事務性的支持,讓我們免去了很多的異常邏輯處理。對於Lua的語法不是本文的主要內容,

正式開發

到這裡,我們正式開始手寫限流組件的進程。

1. 工程定義

項目基於maven構建,主要依賴Spring-boot-starter,我們主要在springboot上進行開發,因此自定義的開發包可以直接依賴下面這個座標,方便進行包管理。版本號自行選擇穩定版。

2. Redis整合

由於我們是基於Redis進行的限流操作,因此需要整合Redis的類庫,上面已經講到,我們是基於Springboot進行的開發,因此這裡可以直接整合RedisTemplate。

百度架構師分享源碼實戰篇:通過分佈式系統解決限流問題

2.1 座標引入

這裡我們引入spring-boot-starter-redis的依賴。

2.2 注入CacheManager及RedisTemplate

新建一個Redis的配置類,命名為RedisCacheConfig,使用javaconfig形式注入CacheManager及RedisTemplate。為了操作方便,我們採用了Jackson進行序列化。代碼如下

注意要使用@Configuration 標註此類為一個配置類,當然你可以使用@Component , 但是不推薦,原因在於@Component 註解雖然也可以當作配置類,但是並不會為其生成CGLIB代理Class,而使用@Configuration ,CGLIB會為其生成代理類,進行性能的提升。

2.3 調用方application.propertie需要增加Redis配置

我們的包開發完畢之後,調用方的application.properties需要進行相關配置如下:

如果有密碼的話,配置password即可。

這裡為單機配置,如果需要支持哨兵集群,則配置如下,Java代碼不需要改動,只需要變動配置即可。注意 兩種配置不能共存!

3. 定義註解

為了調用方便,我們定義一個名為RateLimiter 的註解,內容如下

該註解明確只用於方法,主要有三個屬性。

1、key–表示限流模塊名,指定該值用於區分不同應用,不同場景,推薦格式為:應用名:模塊名:ip:接口名:方法名 2、limit–表示單位時間允許通過的請求數 3、expire–incr的值的過期時間,業務中表示限流的單位時間。

"
\t來源:http://dwz.win/dmj

在分佈式領域,我們難免會遇到併發量突增,對後端服務造成高壓力,嚴重甚至會導致系統宕機。為避免這種問題,我們通常會為接口添加限流、降級、熔斷等能力,從而使接口更為健壯。Java領域常見的開源組件有Netflix的hystrix,阿里系開源的sentinel等,都是蠻不錯的限流熔斷框架。

今天我們就基於Redis組件的特性,實現一個分佈式限流組件,名字就定為shield-ratelimiter。

百度架構師分享源碼實戰篇:通過分佈式系統解決限流問題

原理

首先解釋下為何採用Redis作為限流組件的核心。

通俗地講,假設一個用戶(用IP判斷)每秒訪問某服務接口的次數不能超過10次,那麼我們可以在Redis中創建一個鍵,並設置鍵的過期時間為60秒。

當一個用戶對此服務接口發起一次訪問就把鍵值加1,在單位時間(此處為1s)內當鍵值增加到10的時候,就禁止訪問服務接口。PS:在某種場景中添加訪問時間間隔還是很有必要的。我們本次不考慮間隔時間,只關注單位時間內的訪問次數。

需求

原理已經講過了,說下需求。

1、基於Redis的incr及過期機制開發 2、調用方便,聲明式 3、Spring支持

基於上述需求,我們決定基於註解方式進行核心功能開發,基於Spring-boot-starter作為基礎環境,從而能夠很好的適配Spring環境。

另外,在本次開發中,我們不通過簡單的調用Redis的java類庫API實現對Redis的incr操作。

原因在於,我們要保證整個限流的操作是原子性的,如果用Java代碼去做操作及判斷,會有併發問題。這裡我決定採用Lua腳本進行核心邏輯的定義。

百度架構師分享源碼實戰篇:通過分佈式系統解決限流問題

為何使用Lua

在正式開發前,我簡單介紹下對Redis的操作中,為何推薦使用Lua腳本。

1、減少網絡開銷: 不使用 Lua 的代碼需要向 Redis 發送多次請求, 而腳本只需一次即可, 減少網絡傳輸; 2、原子操作: Redis 將整個腳本作為一個原子執行, 無需擔心併發, 也就無需事務; 3、複用: 腳本會永久保存 Redis 中, 其他客戶端可繼續使用.

Redis添加了對Lua的支持,能夠很好的滿足原子性、事務性的支持,讓我們免去了很多的異常邏輯處理。對於Lua的語法不是本文的主要內容,

正式開發

到這裡,我們正式開始手寫限流組件的進程。

1. 工程定義

項目基於maven構建,主要依賴Spring-boot-starter,我們主要在springboot上進行開發,因此自定義的開發包可以直接依賴下面這個座標,方便進行包管理。版本號自行選擇穩定版。

2. Redis整合

由於我們是基於Redis進行的限流操作,因此需要整合Redis的類庫,上面已經講到,我們是基於Springboot進行的開發,因此這裡可以直接整合RedisTemplate。

百度架構師分享源碼實戰篇:通過分佈式系統解決限流問題

2.1 座標引入

這裡我們引入spring-boot-starter-redis的依賴。

2.2 注入CacheManager及RedisTemplate

新建一個Redis的配置類,命名為RedisCacheConfig,使用javaconfig形式注入CacheManager及RedisTemplate。為了操作方便,我們採用了Jackson進行序列化。代碼如下

注意要使用@Configuration 標註此類為一個配置類,當然你可以使用@Component , 但是不推薦,原因在於@Component 註解雖然也可以當作配置類,但是並不會為其生成CGLIB代理Class,而使用@Configuration ,CGLIB會為其生成代理類,進行性能的提升。

2.3 調用方application.propertie需要增加Redis配置

我們的包開發完畢之後,調用方的application.properties需要進行相關配置如下:

如果有密碼的話,配置password即可。

這裡為單機配置,如果需要支持哨兵集群,則配置如下,Java代碼不需要改動,只需要變動配置即可。注意 兩種配置不能共存!

3. 定義註解

為了調用方便,我們定義一個名為RateLimiter 的註解,內容如下

該註解明確只用於方法,主要有三個屬性。

1、key–表示限流模塊名,指定該值用於區分不同應用,不同場景,推薦格式為:應用名:模塊名:ip:接口名:方法名 2、limit–表示單位時間允許通過的請求數 3、expire–incr的值的過期時間,業務中表示限流的單位時間。

百度架構師分享源碼實戰篇:通過分佈式系統解決限流問題

4. 解析註解

定義好註解後,需要開發註解使用的切面,這裡我們直接使用aspectj進行切面的開發。先看代碼

這裡是注入了RedisTemplate,使用其API進行Lua腳本的調用。

init() 方法在應用啟動時會初始化DefaultRedisScript,並加載Lua腳本,方便進行調用。

PS: Lua腳本放置在classpath下,通過ClassPathResource進行加載。

這裡我們定義了一個切點,表示只要註解了@RateLimiter 的方法,均可以觸發限流操作。

這段代碼的邏輯為,獲取 @RateLimiter 註解配置的屬性:key、limit、expire,並通過redisTemplate.execute(RedisScriptscript,Listkeys,Object…args) 方法傳遞給Lua腳本進行限流相關操作,邏輯很清晰。

這裡我們定義如果腳本返回狀態為0則為觸發限流,1表示正常請求。

5. Lua腳本

這裡是我們整個限流操作的核心,通過執行一個Lua腳本進行限流的操作。腳本內容如下

邏輯很通俗,我簡單介紹下。

1、首先腳本獲取Java代碼中傳遞而來的要限流的模塊的key,不同的模塊key值一定不能相同,否則會覆蓋!2、redis.call(‘incr’, key1)對傳入的key做incr操作,如果key首次生成,設置超時時間ARGV[1];(初始值為1) 3、ttl是為防止某些key在未設置超時時間並長時間已經存在的情況下做的保護的判斷;4、每次請求都會做+1操作,當限流的值val大於我們註解的閾值,則返回0表示已經超過請求限制,觸發限流。否則為正常請求。

當過期後,又是新的一輪循環,整個過程是一個原子性的操作,能夠保證單位時間不會超過我們預設的請求閾值。

到這裡我們便可以在項目中進行測試。

測試

這裡我貼一下核心代碼,我們定義一個接口,並註解@RateLimiter(key=“ratedemo:1.0.0”,limit=5,expire=100) 表示模塊ratedemo:sendPayment:1.0.0 在100s內允許通過5個請求,這裡的參數設置是為了方便看結果。實際中,我們通常會設置1s內允許通過的次數。

我們通過RestClient請求接口,日誌返回如下:

根據日誌能夠看到,正常請求5次後,返回限流觸發,說明我們的邏輯生效,對前端而言也是可以看到false標記,表明我們的Lua腳本限流邏輯是正確的,這裡具體返回什麼標記需要調用方進行明確的定義。

總結

我們通過Redis的incr及expire功能特性,開發定義了一套基於註解的分佈式限流操作,核心邏輯基於Lua保證了原子性。達到了很好的限流的目的,生產上,可以基於該特點進行定製自己的限流組件,當然你可以參考本文的代碼,相信你寫的一定比我的demo更好!

end:如果你覺得本文對你有幫助的話,記得關注點贊轉發,你的支持就是我更新動力。

"

相關推薦

推薦中...