服務限頻限次的場景方案

夢之痕發表於2019-04-03

概述

在設計服務的時候,我們會遇到不同的限速限頻需求。根據不同的需求場景,我們會有對應的解決方案。

服務限速

需求場景

  • 單機需要限制某些程式碼塊的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複雜,不過可以減少空間佔用;

小結

以上是我們常見的限速限頻需求,本文簡單介紹一些基本的思路。在實際應用場景中或許有更巧妙的解決方案,可以一起溝通交流。

相關文章