限流的簡單使用及學習


前言

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

整個系統間的調用都是采用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),從而平滑突發流入速率;
  • 令牌桶允許一定程度的突發,而漏桶主要目的是平滑流入速率;
  • 兩個算法實現可以一樣,但是方向是相反的,對於相同的參數得到的限流效果是一樣的。




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM