服務限頻限次的場景方案

概述

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

服務限速

需求場景

  • 單機須要限制某些代碼塊的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消費、定時任務之類)。併發

時間間隔的次數限制

需求場景

  • 高頻次場景app

    如1天內接口請求次數;分佈式

  • 低頻次場景工具

    如1個小時內用戶只能發5條feed動態;ui

解決方案

  • 高頻次場景(Redis Counter)spa

    在高頻次場景咱們通常都不會太關注具體的限頻次數,因此在計數上不會要求特別準確。線程

    咱們能夠經過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複雜,不過能夠減小空間佔用;

小結

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

相關文章
相關標籤/搜索