1. 程式人生 > >限流的簡單使用及學習

限流的簡單使用及學習

前言

最近系統剛做了一次大的重構,以及下游子服務都做了升級改造。

整個系統間的呼叫都是採用spring cloud這一套去實現的。我所負責的為業務服務端,專門為web端和pc端提供介面呼叫。在服務剛上線的一段時間,出現了一次雪崩的事件,整個呼叫鏈路如下:

呼叫鏈路很簡單,因為文字匹配服務 需要分詞,匹配,已經從ES獲取匹配後的術語語料等資料,所以會有請求擠壓,一段時間類服務就崩潰了。為了緊急處理這種情況,所以需要再業務方加上限流機制(後續優化下游的匹配演算法)。剛好也針對於這種情況,自己來學習下幾種限流的方式。

限流演算法分類

參見的限流演算法有:令牌桶,漏桶,計數器。

計數器限流演算法

計數器是最簡單也是最粗暴的一種限流演算法,同時也是比較常用的,主要用來限制總併發數,比如資料庫連線池大小、執行緒池大小、程式訪問併發數等都是使用計數器演算法。

  1. 使用Redis的限流做法:
/**
 * 限流方法,通過redis進行方法級別的限流措施。
 */
@Service
@Transactional
@Slf4j
public class MethodThrottleService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 通過指定key值獲取是否是合法請求,如果在規定快取時間內仍然存在該key值,說明該請求不合法
     *
     * @param key        請求key值
     * @param expireTime 過期時間
     * @param timeUnit   過期時間單位
     * @return 是否過期 true || false
     */
    public Boolean validateKeyRequest(String key, int expireTime, TimeUnit timeUnit) {
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        String result = ops.get(key);
        if (StringUtils.isNotBlank(result)) {
            return false;
        }

        ops.set(key, key, expireTime, timeUnit);
        return true;
    }

    /**
     * 通過指定使用者和方法名判斷請求是否合法請求,如果在規定快取時間內仍然存在該key值,說明該請求不合法
     *
     * @param methodName 方法名
     * @param perCount   規定時間請求的次數
     * @param iolId      使用者名稱
     * @return 是否過期 true || false
     */
    public Boolean validateUserRequest(String methodName, int perCount, String iolId, int expireTime, TimeUnit timeUnit) {
        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        String cacheKey = getCacheKey(iolId, methodName);
        Long requestCount = ops.increment(cacheKey, 1);
        log.info("requestCount = {}", requestCount);
        redisTemplate.expire(cacheKey,expireTime, timeUnit );
        if (requestCount >= perCount) {
            log.info("MethodThrottle exceed weight limit! iolId = {}, methodName = {}, requestCount = {}", iolId, methodName, requestCount);
            return false;
        }

        return true;
    }

    /**
     * 獲取快取的key值
     * @param targetName 目標名稱
     * @param methodName 方法名稱
     * @return 快取key
      */
    private String getCacheKey(String targetName, String methodName) {
        StringBuilder sb = new StringBuilder("");
        sb.append("limitRate.").append(targetName).append(".").append(methodName);
        return sb.toString();
    }
}

使用redis限流,可以針對於使用者+方法名進行精準限流。同時可以根據請求key值進行限流,目的是限定規定時間類同樣引數的請求次數。
但是redis 限流會有很大的效能瓶頸,頻繁的寫入,讀取,過期會對redis效能損耗比較大。不建議此種方法。
另外計數器還可以使用AtomicIntegerSemaphore,具體就不在這列出程式碼了,具體可以參考:Java限流策略-簡書

令牌桶演算法

令牌桶演算法是一個存放固定容量的令牌的桶,按照固定速率往桶裡新增令牌。令牌桶演算法的描述如下:(參考開濤:億級流量網站架構核心技術 中第4章部分內容)
如下:

  • 假設限制2r/s,則按照500毫秒的固定速率往桶中新增令牌;
  • 桶中最多存放b個令牌,當桶滿時,新新增的令牌被丟棄或拒絕;
    -當一個n個位元組大小的資料包到達,將從桶中刪除n個令牌,接著資料包被髮送到網路上;
    -如果桶中的令牌不足n個,則不會刪除令牌,且該資料包將被限流(要麼丟棄,要麼緩衝區等待)。


備註(10r/s: 一秒鐘10令牌放入桶中)
對於令牌桶限流,我們可以使用Guava開源得到RateLimiter 來做,具體可以參考如下程式碼:

//每秒只發出10個令牌
RateLimiter rateLimiter = RateLimiter.create(10);
/**
 * 嘗試獲取令牌
 *
 * @return 獲取令牌是否成功 true || false
 */
public boolean tryAcquire() {
    return rateLimiter.tryAcquire();
}

漏桶演算法

漏桶作為計量工具(The Leaky Bucket Algorithm as a Meter)時,可以用於流量整形(Traffic Shaping)和流量控制(TrafficPolicing),漏桶演算法的描述如下:

  • 一個固定容量的漏桶,按照常量固定速率流出水滴;
  • 如果桶是空的,則不需流出水滴;
  • 可以以任意速率流入水滴到漏桶;
  • 如果流入水滴超出了桶的容量,則流入的水滴溢位了(被丟棄),而漏桶容量是不變的。

令牌桶和漏桶對比:

  • 令牌桶是按照固定速率往桶中新增令牌,請求是否被處理需要看桶中令牌是否足夠,當令牌數減為零時則拒絕新的請求;
  • 漏桶則是按照常量固定速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕;
  • 令牌桶限制的是平均流入速率(允許突發請求,只要有令牌就可以處理,支援一次拿3個令牌,4個令牌),並允許一定程度突發流量;
  • 漏桶限制的是常量流出速率(即流出速率是一個固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),從而平滑突發流入速率;
  • 令牌桶允許一定程度的突發,而漏桶主要目的是平滑流入速率;
  • 兩個演算法實現可以一樣,但是方向是相反的,對於相同的引數得到的限流效果是一樣的。