Redis分布式限流的幾種實(shí)現(xiàn)
一、 簡(jiǎn)介
分布式限流是指通過(guò)將限流策略嵌入到分布式系統(tǒng)中,以控制流量或保護(hù)服務(wù),保證系統(tǒng)在高并發(fā)訪問(wèn)情況下不被過(guò)載。
分布式限流可以防止系統(tǒng)因大量請(qǐng)求同時(shí)到達(dá)導(dǎo)致壓力過(guò)大而崩潰,從而提高系統(tǒng)的穩(wěn)定性和可靠性。同時(shí),它可以使得業(yè)務(wù)資源能夠更好地分配,提高系統(tǒng)的效率。
二、分布式限流
1 數(shù)據(jù)結(jié)構(gòu)
1.1 Redis List
Redis List 是一個(gè)可以輸入相同元素和非唯一元素的集合且支持在兩端進(jìn)行快速(O(1))插入和刪除元素。
1.2 Redis Set
Redis Set 是一個(gè)無(wú)序,但不允許重復(fù)元素的集合。您可以使用這些命令對(duì)Redis集進(jìn)行常規(guī)操作: SADD,SREM,SISMEMBER,SMEMBERS等。
1.3 Redis Sorted Set
Redis Sorted Set 是一個(gè)有序的、不重復(fù)的元素集合。每個(gè)元素都關(guān)聯(lián)著一個(gè)浮點(diǎn)數(shù)值(稱(chēng)為 Score)。通過(guò) Score 可以從小到大排序得到一個(gè)有序集合。
2 實(shí)現(xiàn)分布式限流
在Redis中可以使用令牌桶算法來(lái)實(shí)現(xiàn)分布式限速。具體方法為:
Step 1:創(chuàng)建一個(gè)列表作為Redis令牌桶
String key = "rate_limit:" + userId; // 模擬用戶(hù)請(qǐng)求訪問(wèn) List<String> tokens = redis.lrange(key, 0, -1);
Step 2:設(shè)定令牌桶的基準(zhǔn)參數(shù)
int maxTokens = 50; long timeInterval = 1 * 1000; long now = System.currentTimeMillis();
Step 3:計(jì)算Redis中令牌數(shù)量
long expiredTokens = tokens.stream().filter(t -> Long.parseLong(t) < now - timeInterval).count(); tokens = tokens.subList((int) expiredTokens, tokens.size()); long remainTokens = maxTokens - tokens.size();
Step 4:基于令牌數(shù)量,判斷是否超過(guò)限制
if (remainTokens < 1) {
throw new RateLimitException("請(qǐng)求太頻繁,請(qǐng)稍后再試!");
}
Step 5:如果沒(méi)有超出限制,則更新Redis中令牌數(shù)并設(shè)置過(guò)期時(shí)間
Long expiresIn = now + timeInterval; redis.multi(); redis.rpush(key, String.valueOf(expiresIn)); redis.pexpire(key, timeInterval); redis.exec();
3 實(shí)現(xiàn)原理分析
以上代碼所示首先需要建立Redis List用于存儲(chǔ)Token,其次需要設(shè)定令牌桶的基準(zhǔn)參數(shù)(比如最大Token數(shù)量和Token過(guò)期間隔等)。在用戶(hù)訪問(wèn)請(qǐng)求時(shí),需要計(jì)算Redis中的令牌數(shù)量,根據(jù)規(guī)則對(duì)訪問(wèn)量進(jìn)行限制。如果沒(méi)有超過(guò)限制,則需要更新Redis List中令牌數(shù)并設(shè)置過(guò)期時(shí)間;如果超過(guò)了限制,則需要返回錯(cuò)誤信息并拒絕服務(wù)。
整個(gè)過(guò)程中,需要注意并發(fā)訪問(wèn)情況下的線程安全問(wèn)題,并確保流量控制配置的公共協(xié)商,如最大QPS(Queries Per Second),哪些接口需限制流量等。
三、分布式限流算法
在實(shí)際的系統(tǒng)設(shè)計(jì)中,為了防止某一時(shí)刻出現(xiàn)大量請(qǐng)求導(dǎo)致系統(tǒng)崩潰,我們通常會(huì)采用限流策略來(lái)控制流量,而Redis作為分布式NoSQL數(shù)據(jù)庫(kù),在限流中也有著廣泛的應(yīng)用。下面介紹一些Redis分布式限流的經(jīng)典算法。
1. 計(jì)數(shù)器算法
計(jì)數(shù)器算法比較簡(jiǎn)單,直接利用Redis存儲(chǔ)每個(gè)IP或者用戶(hù)的請(qǐng)求次數(shù),當(dāng)請(qǐng)求次數(shù)超過(guò)預(yù)設(shè)閾值時(shí)拒絕服務(wù)。代碼如下:
public boolean isAllowed(String key, int limit, int timeout) {
Jedis jedis = getJedis();
long count = jedis.incr(key);
if (count == 1) {
jedis.expire(key, timeout);
}
boolean allowed = count <= limit;
if (!allowed) {
jedis.del(key);
}
jedis.close();
return allowed;
}
key:需要限流的用戶(hù)標(biāo)識(shí),可根據(jù)IP、UserID等進(jìn)行定義limit:閾值,即允許的最大請(qǐng)求數(shù)timeout:過(guò)期時(shí)間,對(duì)于計(jì)數(shù)器算法,一定要設(shè)置過(guò)期時(shí)間,否則緩存中的請(qǐng)求次數(shù)會(huì)一直不斷累加
2. 漏斗算法
漏斗算法的核心思想是將請(qǐng)求按照恒定的速率轉(zhuǎn)換為水流,有效控制請(qǐng)求超出服務(wù)處理能力的情況。漏斗算法實(shí)現(xiàn)代碼如下:
public boolean isAllowed(String key, int capacity, double leakRate, int reqCount) {
Jedis jedis = getJedis();
long nowTime = System.currentTimeMillis();
String luaScript =
"local currentCapacity = tonumber(redis.call('hget', KEYS[1], 'leftCapacity'))\n"
+ "if currentCapacity == nil then\n"
+ " redis.call('hset', KEYS[1], 'lastTime', ARGV[2])\n"
+ " redis.call('hset', KEYS[1], 'leftCapacity', ARGV[1] - 1)\n"
+ " return 1\n"
+ "end\n"
+ "local changeTime = tonumber(redis.call('hget', KEYS[1], 'lastTime'))\n"
+ "local delayMillSeconds = nowTime - changeTime\n"
+ "local currentDelayCount = tonumber(delayMillSeconds*ARGV[3])\n"
+ "local currentCapacity = math.min(currentDelayCount+currentCapacity, ARGV[1])\n"
+ "if currentCapacity >= ARGV[4] then\n"
+ " return 0\n"
+ "else\n"
+ " redis.call('hset', KEYS[1], 'leftCapacity', currentCapacity-1)\n"
+ " redis.call('hset', KEYS[1], 'lastTime', nowTime)\n"
+ " return 1\n"
+ "end";
Object result = jedis.eval(
luaScript,
Collections.singletonList(key),
Arrays.asList(String.valueOf(capacity), String.valueOf(nowTime), String.valueOf(leakRate), String.valueOf(reqCount))
);
boolean allowed = (result instanceof Long ? (Long) result : 0L) == 1L;
jedis.close();
return allowed;
}
key:需要進(jìn)行限流的用戶(hù)標(biāo)識(shí)capacity:漏斗容量,即最大允許請(qǐng)求數(shù)量leakRate:漏嘴流水速率,保證有序的請(qǐng)求到達(dá)reqCount:預(yù)計(jì)請(qǐng)求量,用于計(jì)算漏斗每次流出的數(shù)量
3. 令牌桶算法
令牌桶算法的特點(diǎn)是以一個(gè)固定的速率不斷產(chǎn)生令牌,并將令牌放入到桶中,訪問(wèn)時(shí)若桶為空,則表示請(qǐng)求數(shù)超限。令牌桶算法實(shí)現(xiàn)代碼如下:
public boolean isAllowed(String key, int capacity, double rate, int reqCount) {
long nowTime = System.currentTimeMillis();
Jedis jedis = getJedis();
String luaScript =
"local currentLimit = tonumber(redis.call('get', KEYS[1]) or '0')\n"
+ "if currentLimit + ARGV[1] > tonumber(KEYS[2]) then\n"
+ " return false\n"
+ "else\n"
+ " redis.call('incrby', KEYS[1], ARGV[1])\n"
+ " redis.call('expire', KEYS[1], ARGV[2])\n"
+ " return true\n"
+ "end";
Object result = jedis.eval(luaScript, 2, key, String.valueOf(capacity), String.valueOf(reqCount), String.valueOf(rate * (nowTime / 1000)));
boolean allowed = (result instanceof Boolean ? (Boolean) result : false);
jedis.close();
return allowed;
}
key:需要進(jìn)行限流的用戶(hù)標(biāo)識(shí)capacity:桶容量rate:令牌發(fā)放速率reqCount:請(qǐng)求數(shù)量
四、分布式限流實(shí)戰(zhàn)
1. 單機(jī)限流實(shí)現(xiàn)
假設(shè)我們有一個(gè)需求,需要限制每個(gè)IP一分鐘內(nèi)最多只能發(fā)送100個(gè)請(qǐng)求??梢酝ㄟ^(guò)Redis的INCR、EXPIRE等API操作來(lái)簡(jiǎn)單實(shí)現(xiàn)單機(jī)限流。
public boolean isAllowed(String ip, int limit, int interval) {
Jedis jedis = getJedis();
String key = "ip:" + ip;
long count = jedis.incr(key);
if (count == 1) {
jedis.expire(key, interval);
}
boolean allowed = count <= limit;
if (!allowed) {
jedis.del(key);
}
jedis.close();
return allowed;
}
2. 基于Redis Clusters的分布式限流實(shí)現(xiàn)
當(dāng)業(yè)務(wù)規(guī)模擴(kuò)大時(shí),單機(jī)的限流已經(jīng)無(wú)法滿(mǎn)足需求,這時(shí)候需要考慮使用Redis Clusters實(shí)現(xiàn)分布式限流。Clusers擴(kuò)展了原先Redis的功能,不僅支持橫向擴(kuò)展,而且提高了整個(gè)集群的可用性。限流算法同上,只是需要使用把數(shù)據(jù)分配到Cluser內(nèi)不同的節(jié)點(diǎn)上。
public boolean isAllowed(String ip, int limit, int interval) {
JedisCluster jedis = getJedisCluster();
String key = "ip:" + ip;
long count = jedis.incr(key);
if (count == 1) {
jedis.expire(key, interval);
}
boolean allowed = count <= limit;
if (!allowed) {
jedis.del(key);
}
return allowed;
}
五、基于Redis分布式限流的優(yōu)化
1. 緩存擊穿
1.1 問(wèn)題描述
在高并發(fā)場(chǎng)景下,如果存在大量的緩存未命中請(qǐng)求,將會(huì)導(dǎo)致訪問(wèn)底層數(shù)據(jù)存儲(chǔ)系統(tǒng),這種情況被稱(chēng)為緩存擊穿。
1.2 解決方案
1.2.1 使用互斥鎖
/**
* 獲取緩存值方法
* @param key 緩存鍵值
* @return 緩存值
*/
public String getCacheValue(String key) {
String value = cache.get(key);
if (value == null) { //緩存未命中
//使用互斥鎖
Lock lock = redisson.getLock(key);
if (lock.tryLock()) { //嘗試獲取鎖
try {
value = cache.get(key); //再次嘗試獲取緩存
if (value == null) { //如果仍然未命中,從數(shù)據(jù)庫(kù)中獲取
value = db.get(key);
cache.put(key, value); //將查詢(xún)結(jié)果放入緩存
}
} finally {
lock.unlock(); //釋放鎖
}
} else {
Thread.sleep(100); //自旋一段時(shí)間后重試
return getCacheValue(key);
}
}
return value;
}
1.2.2 使用預(yù)熱機(jī)制
預(yù)熱機(jī)制是指在系統(tǒng)啟動(dòng)的時(shí)候,提前加載熱點(diǎn)數(shù)據(jù)到緩存中,以減少緩存未命中請(qǐng)求。預(yù)熱的方式可以使用定時(shí)任務(wù)或者其他方式,在系統(tǒng)低峰期進(jìn)行加載。
2. 熱點(diǎn)key問(wèn)題的解決方案
2.1 問(wèn)題描述
在高并發(fā)場(chǎng)景下,如果某個(gè)key的請(qǐng)求量過(guò)大,將會(huì)導(dǎo)致這個(gè)key成為熱點(diǎn)key,從而導(dǎo)致緩存雪崩問(wèn)題。
2.2 解決方案
2.2.1 分布式鎖
/**
* 獲取緩存值方法
* @param key 緩存鍵值
* @return 緩存值
*/
public String getCacheValue(String key) {
String value = cache.get(key);
if (value == null) { //緩存未命中
//使用分布式鎖
RLock lock = redisson.getFairLock(key);
if (lock.tryLock()) { //嘗試獲取鎖
try {
value = cache.get(key); //再次嘗試獲取緩存
if (value == null) { //如果仍然未命中,從數(shù)據(jù)庫(kù)中獲取
value = db.get(key);
cache.put(key, value); //將查詢(xún)結(jié)果放入緩存
}
} finally {
lock.unlock(); //釋放鎖
}
} else {
Thread.sleep(100); //自旋一段時(shí)間后重試
return getCacheValue(key);
}
}
return value;
}
2.2.2 分布式緩存
使用分布式緩存將數(shù)據(jù)均勻地分散到多個(gè)節(jié)點(diǎn)上,從而避免單點(diǎn)瓶頸問(wèn)題。
3. 并發(fā)競(jìng)爭(zhēng)優(yōu)化
3.1 問(wèn)題描述
在高并發(fā)場(chǎng)景下,對(duì)于某些資源的并發(fā)訪問(wèn)將會(huì)導(dǎo)致性能瓶頸,需要進(jìn)行并發(fā)競(jìng)爭(zhēng)優(yōu)化。
3.2 解決方案
3.2.1 使用限流器
限流器類(lèi)似于信號(hào)燈,用于控制并發(fā)請(qǐng)求的數(shù)量,在高峰期可以采用漏桶算法或令牌桶算法進(jìn)行限流。
3.2.2 使用異步線程池
對(duì)于一些耗時(shí)的操作,可以使用異步線程池進(jìn)行處理,從而避免阻塞主線程,提升系統(tǒng)的并發(fā)能力。
到此這篇關(guān)于Redis分布式限流的幾種實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Redis分布式限流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis和數(shù)據(jù)庫(kù) 數(shù)據(jù)同步問(wèn)題的解決
這篇文章主要介紹了Redis和數(shù)據(jù)庫(kù) 數(shù)據(jù)同步問(wèn)題的解決操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-01-01
了解Redis常見(jiàn)應(yīng)用場(chǎng)景
Redis是一個(gè)key-value存儲(chǔ)系統(tǒng),現(xiàn)在在各種系統(tǒng)中的使用越來(lái)越多,大部分情況下是因?yàn)槠涓咝阅艿奶匦裕划?dāng)做緩存使用,這里介紹下Redis經(jīng)常遇到的使用場(chǎng)景2021-06-06
Redis簡(jiǎn)易延時(shí)隊(duì)列的實(shí)現(xiàn)示例
在實(shí)際的業(yè)務(wù)場(chǎng)景中,經(jīng)常會(huì)遇到需要延時(shí)處理的業(yè)務(wù),本文就來(lái)介紹有下Redis簡(jiǎn)易延時(shí)隊(duì)列的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12
Redis的Zset類(lèi)型及相關(guān)命令詳細(xì)講解
這篇文章主要介紹了Redis的Zset類(lèi)型及相關(guān)命令的相關(guān)資料,有序集合Zset是一種Redis數(shù)據(jù)結(jié)構(gòu),它類(lèi)似于集合Set,但每個(gè)元素都有一個(gè)關(guān)聯(lián)的分?jǐn)?shù)score,并且可以根據(jù)分?jǐn)?shù)對(duì)元素進(jìn)行排序,需要的朋友可以參考下2025-01-01
基于Redis有序集合實(shí)現(xiàn)滑動(dòng)窗口限流的步驟
滑動(dòng)窗口算法是一種基于時(shí)間窗口的限流算法,通過(guò)動(dòng)態(tài)地滑動(dòng)窗口,可以動(dòng)態(tài)調(diào)整限流的速率,Redis有序集合可以用來(lái)實(shí)現(xiàn)滑動(dòng)窗口限流,本文介紹基于Redis有序集合實(shí)現(xiàn)滑動(dòng)窗口限流,感興趣的朋友一起看看吧2024-12-12

