前言
隻有光頭才能變強。
文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/ZhongFuCheng3y/3y
之前在學習的時候也接觸不到高并發/大流量這種東西,是以限流當然是沒接觸過的了。在看公司項目的時候,發現有用到限流(RateLimiter),順帶了解一波。
一、限流基礎知識介紹
為啥要限流,相信就不用我多說了。
- 比如,我周末去飯店吃飯,但是人太多了,我隻能去前台拿個号,等号碼到我的時候才能進飯店吃飯。如果飯店沒有限流怎麼辦?一到飯點,人都往裡沖,而飯店又處理不了這麼多人流,很容易就出事故(飯店塞滿了人,無路可走。飯店的從業人員崩潰了,處理不過來)
- 回到代碼世界上也是一樣的,伺服器能處理的請求數有限,如果請求量特别大,我們需要做限流(要麼就讓請求等待,要麼就把請求給扔了)
在代碼世界上,限流有兩種比較常見的算法:
- 令牌桶算法
- 漏桶算法
1.1 什麼是漏桶算法
比如,現在我有一個桶子,綠色那塊是我能裝水的容量,如果超過我能裝下的容量,再往桶子裡邊倒水,就會溢出來(限流):
我們目前可以知道的是:
- 桶子的容量是固定的(是圖上綠色那塊)
- 超出了桶子的容量就會溢出(要麼等待,要麼直接丢棄)
OK,現在我們在桶子裡挖個洞,讓水可以從洞子裡邊流出來:
桶子的洞口的大小是固定的,是以水從洞口流出來的速率也是固定的。
是以總結下來算法所需的參數就兩個:
- 桶子的容量
- 漏水的速率
漏桶算法有兩種實作:
- 不允許突發流量的情況:如果進水的速率大于出水的速率,直接舍棄掉多餘的水。比如,我的桶子容量能裝100L,但我的桶子出水速率是10L/s。此時,如果現在有100L/s的水進來,我隻讓10L的水進到桶子,其餘的都限流。(限定了請求的速度)
- 允許一定的突發流量情況:我的桶子能裝100L,如果現在我的桶子是空的,那麼這100L的水都能進我的桶子。我以10L/s的速率将這些水流出,如果還有100L的水進來,隻能限流了。
經過上面的分析我們就知道:
漏桶算法可以平滑網絡上的突發流量(因為漏水的速率是固定的)
1.2 什麼是令牌桶算法
現在我有另外一個桶子,這個桶子不用來裝水,用來裝令牌:
令牌會一定的速率扔進桶子裡邊,比如我1秒扔10個令牌進桶子:
桶子能裝令牌的個數有上限的,比如我的桶子最多隻能裝1000個令牌。
每個請求進來,就會去桶子拿一個令牌
- 比如這秒我有1001個請求,我就去桶子裡邊拿1001個令牌,此時可能會出現兩種情況:
- 桶子裡邊沒有1001個令牌,隻有1000個,那沒拿到令牌的請求隻能被阻塞了(等待)
- 桶子裡邊有1001個令牌,所有請求都可以執行。
令牌桶算法支援網絡上的突發流量
漏桶和令牌桶的差別:從上面的例子估計大家也能看出來了,漏桶隻能以固定的速率去處理請求,而令牌桶可以以桶子最大的令牌數去處理請求
二、RateLimiter使用
RateLimiter是Guava的一個限流元件,我這邊的系統就有用到這個限流元件,使用起來十分友善。
引入pom依賴:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
RateLimiter它是基于令牌桶算法的,API非常簡單,看以下的Demo:
public static void main(String[] args) {
//線程池
ExecutorService exec = Executors.newCachedThreadPool();
//速率是每秒隻有3個許可
final RateLimiter rateLimiter = RateLimiter.create(3.0);
for (int i = 0; i < 100; i++) {
final int no = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
//擷取許可
rateLimiter.acquire();
System.out.println("Accessing: " + no + ",time:"
+ new SimpleDateFormat("yy-MM-dd HH:mm:ss").format(new Date()));
} catch (Exception e) {
e.printStackTrace();
}
}
};
//執行線程
exec.execute(runnable);
}
//退出線程池
exec.shutdown();
}
我們可以從結果看出,每秒隻能執行三個:
三、分布式限流
RateLimiter是一個單機的限流元件,如果是分布式應用的話,該怎麼做?
可以使用Redis+Lua的方式來實作,大緻的lua腳本代碼如下:
local key = "rate.limit:" .. KEYS[1] --限流KEY
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
return 0
else --請求數+1,并設定1秒過期
redis.call("INCRBY", key,"1")
redis.call("expire", key,"1")
return current + 1
end
Java代碼如下:
public static boolean accquire() throws IOException, URISyntaxException {
Jedis jedis = new Jedis("127.0.0.1");
File luaFile = new File(RedisLimitRateWithLUA.class.getResource("/").toURI().getPath() + "limit.lua");
String luaScript = FileUtils.readFileToString(luaFile);
String key = "ip:" + System.currentTimeMillis()/1000; // 目前秒
String limit = "5"; // 最大限制
List<String> keys = new ArrayList<String>();
keys.add(key);
List<String> args = new ArrayList<String>();
args.add(limit);
Long result = (Long)(jedis.eval(luaScript, keys, args)); // 執行lua腳本,傳入參數
return result == 1;
}
解釋:
- Java代碼傳入key和最大的限制limit參數進lua腳本
- 執行lua腳本(lua腳本判斷目前key是否超過了最大限制limit)
- 如果超過,則傳回0(限流)
- 如果沒超過,傳回1(程式繼續執行)
參考來源:
- https://segmentfault.com/a/1190000016552464
更多資料參考:
- https://segmentfault.com/a/1190000016042927
- [http://wuwenliang.net/2018/10/27/%E8%87%AA%E5%B7%B1%E5%86%99%E5%88%86%E5%B8%83%E5%BC%8F%E9%99%90%E6%B5%81%E7%BB%84%E4%BB%B6-%E5%9F%BA%E4%BA%8ERedis%E7%9A%84RateLimter/](http://wuwenliang.net/2018/10/27/自己寫分布式限流元件-基于Redis的RateLimter/)
最後
樂于輸出幹貨的Java技術公衆号:Java3y。公衆号内有200多篇原創技術文章、海量視訊資源、精美腦圖,關注即可擷取!
覺得我的文章寫得不錯,點贊!
更多的文章可往:文章的目錄導航