概述
在設計服務的時候,我們會遇到不同的限速限頻需求。根據不同的需求場景,我們會有對應的解決方案。
服務限速
需求場景
- 單機需要限制某些程式碼塊的QPS,如資料庫的插入操作、Redis寫入操作等;
- 超過指定QPS時自動阻塞執行緒或者自動丟棄指定程式碼塊邏輯;
- 代替自己通過sleep的方式實現的限速方案;
解決方案
使用Guava的RateLimiter工具類,基於令牌桶演算法實現流量限制。
一般地RateLimiter提供的是單機的限流方案。若需要實現服務整體的QPS流量控制,可以基於Redis實現令牌桶演算法。也可以將整體QPS拆分成單個機器的訪問QPS,再進行相應的控制(這種方式QPS控制並不是很準確,尤其是在服務QPS限制比較低的情況下)。
使用方法
private final RateLimiter rateLimiter = RateLimiter.create(100); // 100QPS
// 堵塞限制QPS
void foo1() {
rateLimiter.acquire(); // 在這裡有可能發生堵塞
// 實際執行的邏輯塊
}
// 不堵塞限制QPS
void foo2() {
if (rateLimiter.tryAcquire()) { // 這裡不會發生任何堵塞行為
// QPS之內允許執行的程式碼路徑
} else {
// 超過指定QPS時執行的程式碼路徑
}
}
複製程式碼
注意事項
一般不推薦在同步業務請求中使用堵塞版本的RateLimter進行限速,更多的使用場景是在消費類場景中使用(如Kafka消費、定時任務之類)。
時間間隔的次數限制
需求場景
-
高頻次場景
如1天內介面請求次數;
-
低頻次場景
如1個小時內使用者只能發5條feed動態;
解決方案
-
高頻次場景(Redis Counter)
在高頻次場景我們一般都不會太關注具體的限頻次數,所以在計數上不會要求特別準確。
我們可以通過Redis的Counter對頻次進行統計。如1天內的介面請求次數我們可以類似這樣設計20190403_appkey,在20190403當天所有的介面訪問,都會對這個key進行加1操作,這樣就可以大概統計到當天的所有請求次數,並進行相應的限次邏輯。
private final FastDateFormat fastDateFormat = FastDateFormat.getInstance("MMddHH"); @Resource private RedisClient rc; public long incrCount(String appkey, long time) { String key = key(appkey, time); return rc.incr(key); } private String key(String appkey, long time) { return appkey + "_" + fastDateFormat.format(time); } 複製程式碼
-
低頻次場景
在低頻次場景,因為次數限制比較少,所以要求計數準確。
Redis SortedSet
我們可以使用Redis SortedSet去維護feed集合,member為對應的feed動態ID,score為對應的feed動態釋出時間。在使用者釋出feed動態的時候,先計算有效的元素個數,最後進行相應的限次邏輯。
這裡是任意的時間間隔,而不是固定的時間間隔。假若使用者在0點59分發布了5條feed動態,使用者在1點0分的時候仍不可以釋出,需要等到1點59分的時候才可以解除限制。
@Resource private RedisClient rc; public void add(User user, FeedItem feedItem) { rc.zadd(key(user), feedItem.getCreateTime(), feedItem.getFeedId()); double min = 0; double max = (double) System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1); rc.zremrangeByScore(key(user), min, max); } public List<String> get(User user, long curTime) { double max = curTime; double min = (double) curTime - TimeUnit.HOURS.toMillis(1); Set<byte[]> value = rc.zrevrangeByScore(key(user), max, min); return value.stream().map(RedisUtils::toString).collect(Collectors.toList()); } private String key(User user) { return String.valueOf(user.getUid()); } 複製程式碼
Redis Hash
如果我們的需求變得更復雜,需要根據動態型別限定使用者只能釋出N條feed動態。
我們可以通過Redis的Hash結構來維護使用者最近釋出的N條feed動態,其中field為動態型別,value為feed動態列表。在使用者釋出feed動態的時候,需要獲取對應型別的釋出feed集合,過濾掉過期的feed集合後判斷使用者是否允許釋出。若允許釋出,再把新的feed加入到對應的集合中。(注:過期的feed集合已經過濾,因此資料不會越來越多)
注意事項
-
注意併發,可以通過分散式鎖解決併發訪問的問題。這裡一般是低頻操作,出現併發的可能性較低;
-
使用上比Sorted Set複雜,不過可以減少空間佔用;
-
小結
以上是我們常見的限速限頻需求,本文簡單介紹一些基本的思路。在實際應用場景中或許有更巧妙的解決方案,可以一起溝通交流。