說起 限速
,想必各位不會陌生。通常在一個服務程式當中,限速指的是對同一類請求進行速率的限制,用來防止服務端某些資源被過度消耗,從而保障服務的穩定性。
限速的好處有以下幾點:
-
保護系統穩定性: 限速可以避免系統因過多請求而過載,導致效能下降甚至崩潰。透過限制請求速率,可以平滑地處理請求,保持系統的穩定執行狀態。
-
防止濫用和惡意攻擊: 限速可以有效地防止惡意使用者或自動化工具對系統進行濫用、DoS(拒絕服務)攻擊或暴力破解等行為。透過限制請求速率,可以降低系統遭受攻擊的風險。
- 控制資源消耗: 一些服務或資源可能具有有限的容量或成本,限速可以幫助控制資源的消耗,確保資源被合理分配和利用。例如,限速可以避免資料庫或儲存系統被過度查詢,保護資料庫伺服器的穩定性和效能。
- 提高服務質量: 透過限速可以減少請求的排隊和等待時間,提高系統對正常使用者的響應速度和服務質量。合理的限速策略可以平衡不同使用者和請求之間的競爭,使系統能夠更公平地分配資源。
通常在業務服務研發當中,我們會藉助成熟的框架來實現限流功能,例如下面所列舉的:
- Guava RateLimiter: Guava 是 Google 開發的 Java 核心庫,其中包含了一個名為 RateLimiter 的限流工具類。它基於令牌桶演算法實現了簡單的限流功能,可以輕鬆地控制程式碼的執行速率。
-
Resilience4j: Resilience4j 是一個用於構建彈性和容錯性應用的 Java 庫,其中包含了限流器(Rate Limiter)功能。它提供了多種限流演算法和配置選項,可以靈活地應用於各種場景。
-
Sentinel: Sentinel 是阿里巴巴開源的流量控制框架,提供了流量控制、熔斷降級、系統負載保護等功能。它支援基於 QPS、執行緒數、併發數等多種限流策略,並提供了實時監控和動態配置功能。
-
Hystrix: Hystrix 是 Netflix 開源的容錯框架,提供了限流、熔斷、降級等功能。雖然 Hystrix 已經進入維護模式,但仍然被許多專案廣泛使用。
- Bucket4j: Bucket4j 是一個基於令牌桶演算法的 Java 限流庫,具有簡單易用和高效能的特點。它支援在記憶體、Redis、Hazelcast 等儲存後端進行限流。
雖然這些框架的功能都非常強大,但是在簡單場景當中,我們並不需要非常複雜的功能,只是對介面進行簡單限流,不涉及負載問題、也不存在分散式需求。所以我打算繼續發揮能親自動手的就先試試的精神,自己實現一個限速的功能。
思路
配置管理:使用了一個Map<String, LimitConfig>
來儲存每個限流 key 對應的限流配置。這些配置包括了最大次數和限流時間視窗持續時間。提供了新增限流配置的相關方法,可以為每個限流 key 設定不同的最大次數和時間視窗。在是否被限流判斷方法中,會檢查配置中是否包含指定的限流 key,如果不包含則新增預設配置,以確保每個限流 key 都有相應的配置。
限流狀態管理:使用了三個Map<String, Integer>
來分別記錄每個限流 key 的最後一次請求時間、請求次數以及用於同步的鎖物件。判斷是否限流的方法裡面,會根據當前時間與最後一次請求時間的間隔以及請求次數來判斷是否需要限流。如果超過了限流時間視窗,則重新計數請求次數;如果請求次數超過了最大次數,則需要限流。
執行緒安全性:使用了ReentrantLock
來保證對限流配置和狀態的執行緒安全訪問。例如,在新增配置項方法中使用了一個全域性的寫鎖,以確保新增限流配置時的執行緒安全性。在判斷是否限流的方法中,對於每一個配置項也增加一個 ReentrantLock
保障修改操作的執行緒安全。同時在統計單個介面請求次數的類也用上了 java.util.concurrent.atomic.AtomicInteger
。
程式碼
根據粉絲建議,我加了一些註釋,方便理解和使用。
import com.funtester.frame.SourceCode
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
/**
* 限流工具,支援N/M限流
*/
class RateLimit {
/**
* 總限流配置
*/
Map<String, LimitConfig> config = [:]
/**
* 最後一次請求時間
*/
Map<String, Integer> lastTime = [:]
/**
* 請求次數
*/
Map<String, AtomicInteger> requestTimes = [:]
/**
* 所有key的鎖
*/
Map<String, ReentrantLock> allLock = [:]
/**
* 寫鎖
*/
ReentrantLock writeLock = new ReentrantLock()
/**
* 是否限流
* @param key 限流key
* @return
*/
boolean isLimit(String key) {
if (!config.containsKey(key)) {
addConfig(key, 2, 2) //預設配置
return isLimit(key) //遞迴,初始化配置
}
def mark = SourceCode.getMark()
if (mark - lastTime[key] >= config[key].duration) {//進入下一個限流週期
if (allLock[key].tryLock(1, TimeUnit.SECONDS)) {
if (mark - lastTime[key] >= config[key].duration) {
lastTime[key] = mark
requestTimes[key] = new AtomicInteger(1)
allLock[key].unlock()
return false
} else {
return true
}
}
}
if (requestTimes[key].get() >= config[key].maxTimes) //超過最大次數
return true
requestTimes[key].getAndIncrement() //增加次數
return false
}
/**
* 新增限流配置
* @param key 限流key
* @param maxTimes 最大次數
* @param duration 限流時間,單位秒
* @return
*/
def addConfig(String key, int maxTimes, int duration) {
if (writeLock.tryLock(1, TimeUnit.SECONDS)) {
try {
if (!config.containsKey(key)) {
config[key] = new LimitConfig(maxTimes: maxTimes, duration: duration)
allLock[key] = new ReentrantLock()
lastTime[key] = SourceCode.getMark()
requestTimes[key] = new AtomicInteger(0)
}
} catch (e) {
} finally {
writeLock.unlock()
}
}
}
/**
* 限流配置
*/
static class LimitConfig {
/**
* 最大次數
*/
int maxTimes
/**
* 限流時間計算持續時間,單位秒
*/
int duration
}
}
測試
測試的指令碼如下:
import com.funtester.httpclient.FunHttp
import com.funtester.utils.RateLimit
class Routine extends FunHttp {
static void main(String[] args) {
def limit = new RateLimit()
limit.addConfig("test", 1, 1)
1000.times {
sleep(0.1)
fun {
def limit1 = limit.isLimit("t4est")
if (!limit1) {
output("未限流")
}
}
} }
}
控制檯列印:
22:19:20:545 F-1 未限流
22:19:20:644 F-2 未限流
22:19:22:094 F-8 未限流
22:19:22:195 F-1 未限流
22:19:24:048 F-3 未限流
22:19:24:150 F-4 未限流
22:19:25 uptime:6 s
22:19:25 finished: 49 task
可以看到按照 2/2s 的預設配置生效了。
- 2021 年原創合集
- 2022 年原創合集
- 2023 年原創合集
- 介面功能測試專題
- 效能測試專題
- Java、Groovy、Go、Python
- 單元&白盒&工具合集
- 測試方案&BUG&爬蟲&UI 自動化
- 測試理論雞湯
- 社群風采&影片合集