Este artigo compreende o algoritmo de limitação atual de perguntas de entrevista de alta frequência, do princípio do algoritmo à implementação e, em seguida, à análise comparativa

Leitura recomendada:

Droga, é um cowboy, os programadores têm trabalhado muito nesses algoritmos por 47 dias e os bytes de quatro lados ganharam a oferta

Adoração! O livro de algoritmos de nível mestre de 666 páginas resumido por Byte Great God, elimine LeetCode em minutos

Algoritmo de janela fixa de contador

princípio

O algoritmo de janela fixa do contador é o algoritmo de limitação de corrente mais básico e mais simples. O princípio é contar as solicitações dentro de uma janela de tempo fixa. Se o número de solicitações exceder o limite, a solicitação será descartada; se o limite definido não for atingido, a solicitação é aceita e a contagem é aumentada em 1. Quando a janela de tempo terminar, redefina o contador para 0.

Diagrama esquemático do algoritmo de janela fixa do contador

Implementação e teste de código

Também é relativamente simples de implementar, da seguinte forma:

package project.limiter;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * Project: AllForJava
 * Title:
 * Description:
 * Date: 2020-09-07 15:56
 * Copyright: Copyright (c) 2020
 *
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: [email protected]
* @version 1.0
 **/

public class CounterLimiter {

    private int windowSize; //窗口大小,毫秒为单位
    private int limit;//窗口内限流大小
    private AtomicInteger count;//当前窗口的计数器

    private CounterLimiter(){}

    public CounterLimiter(int windowSize,int limit){
        this.limit = limit;
        this.windowSize = windowSize;
        count = new AtomicInteger(0);

        //开启一个线程,达到窗口结束时清空count
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    count.set(0);
                    try {
                        Thread.sleep(windowSize);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    //请求到达后先调用本方法,若返回true,则请求通过,否则限流
    public boolean tryAcquire(){
        int newCount = count.addAndGet(1);
        if(newCount > limit){
            return false;
        }else{
            return true;
        }
    }

    //测试
    public static void main(String[] args) throws InterruptedException {
        //每秒20个请求
        CounterLimiter counterLimiter = new CounterLimiter(1000,20);
        int count = 0;
        //模拟50次请求,看多少能通过
        for(int i = 0;i < 50;i ++){
            if(counterLimiter.tryAcquire()){
                count ++;
            }
        }
        System.out.println("第一拨50次请求中通过:" + count + ",限流:" + (50 - count));
        //过一秒再请求
        Thread.sleep(1000);
        //模拟50次请求,看多少能通过
        count = 0;
        for(int i = 0;i < 50;i ++){
            if(counterLimiter.tryAcquire()){
                count ++;
            }
        }
        System.out.println("第二拨50次请求中通过:" + count + ",限流:" + (50 - count));
    }

}

Os resultados do teste são os seguintes:

Resultados do teste de algoritmo de janela fixa de contador

Pode-se verificar que apenas 20 das 50 solicitações foram aprovadas e 30 foram restritas, o que atingiu o efeito de restrição atual esperado.

Análise característica

Vantagens : simples de implementar e fácil de entender.

Desvantagens : A curva de vazão pode não ser suficientemente lisa, com "fenômeno de spur", conforme mostrado na figura abaixo. Haverá dois problemas:

Curva de limite de corrente do algoritmo de janela fixa do contador

  1. O serviço do sistema fica indisponível por um período de tempo (não excedendo a janela de tempo) . Por exemplo, o tamanho da janela é 1s, o tamanho limite atual é 100 e, então, 100 solicitações acontecem nos primeiros ms de uma janela e, em seguida, as solicitações de 2 ms a 999 ms serão rejeitadas. Durante esse tempo, os usuários sentirão que o serviço do sistema está indisponível

  2. Quando a janela é comutada, uma solicitação de duas vezes o tráfego de limite pode ser gerada . Por exemplo, o tamanho da janela é 1s, o tamanho limite atual é 100 e, então, 100 solicitações chegam nos 999 ms de uma determinada janela. Não há solicitações no estágio inicial da janela, portanto, todas as 100 solicitações serão aprovadas. Acontece que 100 solicitações vieram no primeiro ms da próxima janela e todas foram aprovadas, ou seja, 200 solicitações passaram em 2 ms, e o limite que definimos foi 100, e as solicitações aprovadas atingiram o limite. Duplo.

    O algoritmo de limitação de corrente da janela fixa do contador gera solicitações para o dobro do fluxo de limite

Algoritmo de janela deslizante do contador

princípio

O algoritmo da janela deslizante do contador é uma melhoria do algoritmo da janela fixa do contador, que resolve a lacuna de duas vezes o limite de tráfego solicitado quando a janela fixa é comutada.

O algoritmo de janela deslizante divide uma janela de tempo em várias pequenas janelas com base em uma janela fixa e, em seguida, cada janela pequena mantém um contador independente. Quando o tempo solicitado é maior que o tempo máximo da janela atual, a janela de tempo é deslocada para a frente por uma pequena janela. Ao fazer a panorâmica, descarte os dados da primeira janela pequena, em seguida, defina a segunda janela pequena como a primeira janela pequena e adicione uma janela pequena no final, e coloque a nova solicitação na janela pequena recém-adicionada . Ao mesmo tempo, é necessário garantir que o número de solicitações para todas as pequenas janelas em toda a janela não possa exceder o limite definido posteriormente.

Diagrama esquemático do algoritmo da janela deslizante do contador

Não é difícil ver na figura que o algoritmo da janela deslizante é uma versão atualizada da janela fixa. Dividindo a janela de tempo em uma pequena janela, o algoritmo de janela deslizante degenera em um algoritmo de janela fixa. Na verdade, o algoritmo da janela deslizante limita o número de solicitações em uma granularidade mais fina. Quanto mais janelas são divididas, mais preciso é o limite atual.

Implementação e teste de código

package project.limiter;

/**
 * Project: AllForJava
 * Title:
 * Description:
 * Date: 2020-09-07 18:38
 * Copyright: Copyright (c) 2020
 *
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: [email protected]
* @version 1.0
 **/

public class CounterSildeWindowLimiter {

    private int windowSize; //窗口大小,毫秒为单位
    private int limit;//窗口内限流大小
    private int splitNum;//切分小窗口的数目大小
    private int[] counters;//每个小窗口的计数数组
    private int index;//当前小窗口计数器的索引
    private long startTime;//窗口开始时间

    private CounterSildeWindowLimiter(){}

    public CounterSildeWindowLimiter(int windowSize, int limit, int splitNum){
        this.limit = limit;
        this.windowSize = windowSize;
        this.splitNum = splitNum;
        counters = new int[splitNum];
        index = 0;
        startTime = System.currentTimeMillis();
    }

    //请求到达后先调用本方法,若返回true,则请求通过,否则限流
    public synchronized boolean tryAcquire(){
        long curTime = System.currentTimeMillis();
        long windowsNum = Math.max(curTime - windowSize - startTime,0) / (windowSize / splitNum);//计算滑动小窗口的数量
        slideWindow(windowsNum);//滑动窗口
        int count = 0;
        for(int i = 0;i < splitNum;i ++){
            count += counters[i];
        }
        if(count >= limit){
            return false;
        }else{
            counters[index] ++;
            return true;
        }
    }

    private synchronized void slideWindow(long windowsNum){
        if(windowsNum == 0)
            return;
        long slideNum = Math.min(windowsNum,splitNum);
        for(int i = 0;i < slideNum;i ++){
            index = (index + 1) % splitNum;
            counters[index] = 0;
        }
        startTime = startTime + windowsNum * (windowSize / splitNum);//更新滑动窗口时间
    }

    //测试
    public static void main(String[] args) throws InterruptedException {
        //每秒20个请求
        int limit = 20;
        CounterSildeWindowLimiter counterSildeWindowLimiter = new CounterSildeWindowLimiter(1000,limit,10);
        int count = 0;

        Thread.sleep(3000);
        //计数器滑动窗口算法模拟100组间隔30ms的50次请求
        System.out.println("计数器滑动窗口算法测试开始");
        System.out.println("开始模拟100组间隔150ms的50次请求");
        int faliCount = 0;
        for(int j = 0;j < 100;j ++){
            count = 0;
            for(int i = 0;i < 50;i ++){
                if(counterSildeWindowLimiter.tryAcquire()){
                    count ++;
                }
            }
            Thread.sleep(150);
            //模拟50次请求,看多少能通过
            for(int i = 0;i < 50;i ++){
                if(counterSildeWindowLimiter.tryAcquire()){
                    count ++;
                }
            }
            if(count > limit){
                System.out.println("时间窗口内放过的请求超过阈值,放过的请求数" + count + ",限流:" + limit);
                faliCount ++;
            }
            Thread.sleep((int)(Math.random() * 100));
        }
        System.out.println("计数器滑动窗口算法测试结束,100组间隔150ms的50次请求模拟完成,限流失败组数:" + faliCount);
        System.out.println("===========================================================================================");


        //计数器固定窗口算法模拟100组间隔30ms的50次请求
        System.out.println("计数器固定窗口算法测试开始");
        //模拟100组间隔30ms的50次请求
        CounterLimiter counterLimiter = new CounterLimiter(1000,limit);
        System.out.println("开始模拟100组间隔150ms的50次请求");
        faliCount = 0;
        for(int j = 0;j < 100;j ++){
            count = 0;
            for(int i = 0;i < 50;i ++){
                if(counterLimiter.tryAcquire()){
                    count ++;
                }
            }
            Thread.sleep(150);
            //模拟50次请求,看多少能通过
            for(int i = 0;i < 50;i ++){
                if(counterLimiter.tryAcquire()){
                    count ++;
                }
            }
            if(count > limit){
                System.out.println("时间窗口内放过的请求超过阈值,放过的请求数" + count + ",限流:" + limit);
                faliCount ++;
            }
            Thread.sleep((int)(Math.random() * 100));
        }
        System.out.println("计数器滑动窗口算法测试结束,100组间隔150ms的50次请求模拟完成,限流失败组数:" + faliCount);
    }
}

No teste, o tamanho da janela deslizante é 1000/10 = 100ms e, em seguida, são simulados 100 conjuntos de 50 solicitações com um intervalo de 150ms. O algoritmo da janela deslizante do contador é comparado com o algoritmo da janela fixa do contador e os seguintes resultados podem ser vistos:

Resultados do teste do algoritmo da janela deslizante do contador

O algoritmo de janela fixa gera um problema de duas vezes a solicitação de tráfego limite quando a janela é alternada, e o algoritmo de janela deslizante evita esse problema.

Análise característica

  1. Evite o problema de que o algoritmo de janela fixa do contador pode gerar duas vezes a solicitação de tráfego de limite quando a janela fixa é comutada;
  2. Em comparação com o algoritmo de funil, novas solicitações também podem ser processadas, evitando o problema de fome do algoritmo de funil.

Algoritmo de funil

princípio

O princípio do algoritmo de funil também é fácil de entender. Depois que a solicitação chegar, ela entrará primeiro no funil e, em seguida, o funil processará o fluxo de saída da solicitação a uma taxa constante, suavizando o fluxo. Quando o tráfego solicitado é muito grande, o funil transborda quando a capacidade máxima é atingida e a solicitação é descartada neste momento. Do ponto de vista do sistema, não sabemos quando haverá solicitações e não sabemos com que rapidez elas chegarão, o que cria perigos ocultos para a segurança do sistema. Mas se você adicionar uma camada de algoritmo de funil para limitar a corrente, poderá garantir que a solicitação flua a uma taxa constante. Do ponto de vista do sistema, a solicitação sempre vem com uma taxa de transmissão suave, protegendo assim o sistema.

Diagrama esquemático do algoritmo de funil

Implementação e teste de código

package project.limiter;

import java.util.Date;
import java.util.LinkedList;

/**
* Project: AllForJava
* Title: 
* Description:
* Date: 2020-09-08 16:45
* Copyright: Copyright (c) 2020
*
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: [email protected]
* @version 1.0
**/
public class LeakyBucketLimiter {

    private int capaticy;//漏斗容量
    private int rate;//漏斗速率
    private int left;//剩余容量
    private LinkedList<Request> requestList;

    private LeakyBucketLimiter() {}

    public LeakyBucketLimiter(int capaticy, int rate) {
        this.capaticy = capaticy;
        this.rate = rate;
        this.left = capaticy;
        requestList = new LinkedList<>();

        //开启一个定时线程,以固定的速率将漏斗中的请求流出,进行处理
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    if(!requestList.isEmpty()){
                        Request request = requestList.removeFirst();
                        handleRequest(request);
                    }
                    try {
                        Thread.sleep(1000 / rate); //睡眠
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    /**
     * 处理请求
     * @param request
     */
    private void handleRequest(Request request){
        request.setHandleTime(new Date());
        System.out.println(request.getCode() + "号请求被处理,请求发起时间:"
                + request.getLaunchTime() + ",请求处理时间:" + request.getHandleTime() + ",处理耗时:"
                + (request.getHandleTime().getTime()  - request.getLaunchTime().getTime()) + "ms");
    }

    public synchronized boolean tryAcquire(Request request){
        if(left <= 0){
            return false;
        }else{
            left --;
            requestList.addLast(request);
            return true;
        }
    }


    /**
     * 请求类,属性包含编号字符串、请求达到时间和请求处理时间
     */
    static class Request{
        private int code;
        private Date launchTime;
        private Date handleTime;

        private Request() { }

        public Request(int code,Date launchTime) {
            this.launchTime = launchTime;
            this.code = code;
        }

        public int getCode() {
            return code;
        }

        public void setCode(int code) {
            this.code = code;
        }

        public Date getLaunchTime() {
            return launchTime;
        }

        public void setLaunchTime(Date launchTime) {
            this.launchTime = launchTime;
        }

        public Date getHandleTime() {
            return handleTime;
        }

        public void setHandleTime(Date handleTime) {
            this.handleTime = handleTime;
        }
    }

    public static void main(String[] args) {
        LeakyBucketLimiter leakyBucketLimiter = new LeakyBucketLimiter(5,2);
        for(int i = 1;i <= 10;i ++){
            Request request = new Request(i,new Date());
            if(leakyBucketLimiter.tryAcquire(request)){
                System.out.println(i + "号请求被接受");
            }else{
                System.out.println(i + "号请求被拒绝");
            }
        }
    }
}

No teste, a capacidade do algoritmo de limitação de corrente do funil é 5 e a taxa do funil é 2 por segundo e, em seguida, 10 solicitações consecutivas, numeradas de 1 a 10, são simuladas e os resultados são os seguintes:

Resultados do teste de algoritmo de funil

Pode-se verificar que as solicitações de 1 a 5 foram aceitas, enquanto as solicitações de 6 a 10 foram rejeitadas, indicando que o funil transbordou neste momento, o que está em linha com nossas expectativas.

Vamos prestar atenção ao processamento das cinco solicitações aceitas. Podemos ver que embora as cinco solicitações sejam aceitas, o processamento é processado uma a uma (não necessariamente em ordem, dependendo da implementação específica), aproximadamente a cada 500ms processando um. Isso reflete as características do algoritmo do funil, ou seja, embora o fluxo da solicitação seja gerado instantaneamente, a solicitação flui a uma taxa fixa e é processada. Como definimos a taxa do funil como 2 por segundo, a cada 500 ms o funil vazará uma solicitação e a processará.

Análise característica

  1. A taxa de vazamento do barril com vazamento é fixa, o que pode desempenhar um papel de retificação . Ou seja, embora o fluxo solicitado possa ser aleatório, grande ou pequeno, após o algoritmo de funil, ele se torna um fluxo estável com taxa fixa, que protege o sistema a jusante.
  2. Não é possível resolver o problema do tráfego repentino . Veja o exemplo que acabamos de testar. Definimos a taxa do funil para 2 por segundo e, de repente, vieram 10 solicitações. Devido à capacidade do funil, apenas 5 solicitações foram aceitas e as outras 5 foram rejeitadas. Você pode dizer que a taxa do funil é de 2 por segundo e, então, 5 solicitações são aceitas instantaneamente. Isso não resolve o problema do tráfego repentino? Não, essas 5 solicitações só foram aceitas, mas não foram processadas imediatamente. A velocidade de processamento ainda é de 2 por segundo conforme definimos, então o problema de estouro de tráfego não foi resolvido. E o algoritmo de token bucket do qual falaremos pode resolver o problema de picos de tráfego até certo ponto. Os leitores podem compará-lo.

Algoritmo de token bucket

princípio

O algoritmo de token bucket é uma melhoria do algoritmo de funil. Além de limitar o fluxo, também permite um certo grau de burst de tráfego. No algoritmo de token bucket, há um token bucket e há um mecanismo no algoritmo para colocar tokens no token bucket a uma taxa constante. O token bucket também tem uma certa capacidade e, se estiver cheio, o token não pode ser colocado nele. Quando uma solicitação chega, ela vai primeiro para o token bucket para obter o token. Se o token for obtido, a solicitação será processada e o token será consumido; se o token bucket estiver vazio, a solicitação será jogar fora.

Diagrama esquemático do algoritmo de token bucket

Implementação e teste de código

package project.limiter;

import java.util.Date;

/**
* Project: AllForJava
* Title: 
* Description:
* Date: 2020-09-08 19:22
* Copyright: Copyright (c) 2020
* 
* @公众号: 超悦编程
* @微信号:exzlco
* @author: 超悦人生
* @email: [email protected]
* @version 1.0
**/
public class TokenBucketLimiter {

    private int capaticy;//令牌桶容量
    private int rate;//令牌产生速率
    private int tokenAmount;//令牌数量

    public TokenBucketLimiter(int capaticy, int rate) {
        this.capaticy = capaticy;
        this.rate = rate;
        tokenAmount = capaticy;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //以恒定速率放令牌
                while (true){
                    synchronized (this){
                        tokenAmount ++;
                        if(tokenAmount > capaticy){
                            tokenAmount = capaticy;
                        }
                    }
                    try {
                        Thread.sleep(1000 / rate);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    public synchronized boolean tryAcquire(Request request){
        if(tokenAmount > 0){
            tokenAmount --;
            handleRequest(request);
            return true;
        }else{
            return false;
        }

    }

    /**
     * 处理请求
     * @param request
     */
    private void handleRequest(Request request){
        request.setHandleTime(new Date());
        System.out.println(request.getCode() + "号请求被处理,请求发起时间:"
                + request.getLaunchTime() + ",请求处理时间:" + request.getHandleTime() + ",处理耗时:"
                + (request.getHandleTime().getTime()  - request.getLaunchTime().getTime()) + "ms");
    }

    /**
     * 请求类,属性只包含一个名字字符串
     */
    static class Request{
        private int code;
        private Date launchTime;
        private Date handleTime;

        private Request() { }

        public Request(int code,Date launchTime) {
            this.launchTime = launchTime;
            this.code = code;
        }

        public int getCode() {
            return code;
        }

        public void setCode(int code) {
            this.code = code;
        }

        public Date getLaunchTime() {
            return launchTime;
        }

        public void setLaunchTime(Date launchTime) {
            this.launchTime = launchTime;
        }

        public Date getHandleTime() {
            return handleTime;
        }

        public void setHandleTime(Date handleTime) {
            this.handleTime = handleTime;
        }
    }


    public static void main(String[] args) throws InterruptedException {
        TokenBucketLimiter tokenBucketLimiter = new TokenBucketLimiter(5,2);
        for(int i = 1;i <= 10;i ++){
            Request request = new Request(i,new Date());
            if(tokenBucketLimiter.tryAcquire(request)){
                System.out.println(i + "号请求被接受");
            }else{
                System.out.println(i + "号请求被拒绝");
            }
        }
    }
}

No teste, a fim de comparar com o algoritmo de limitação de corrente de funil, a capacidade do algoritmo de token bucket também é 5, a taxa de geração de token é 2 por segundo e, em seguida, 10 solicitações consecutivas são simuladas, numeradas de 1-10, Os resultados são os seguintes:

Resultados do teste de algoritmo de token bucket

Pode-se ver que, para 10 solicitações, o algoritmo de token bucket é o mesmo que o algoritmo de funil, 5 solicitações são aceitas e 5 solicitações são rejeitadas. Diferente do algoritmo de funil, o algoritmo de token bucket processa essas 5 solicitações imediatamente, e a velocidade de processamento pode ser considerada como 5 por segundo, o que excede a taxa que definimos 2 por segundo, o que permite um certo grau de burst de tráfego. . Essa também é a principal diferença do algoritmo de funil, que pode ser experimentada com cuidado.

Análise característica

O algoritmo de token bucket é uma melhoria do algoritmo de leaky bucket. Ele pode não apenas limitar a taxa média de chamadas, mas também permitir um certo grau de estouro de tráfego.

resumo

Vamos resumir brevemente os quatro algoritmos de limitação atuais acima.

O algoritmo de janela fixa do contador é simples de implementar e fácil de entender. Em comparação com o algoritmo de funil, novas solicitações também podem ser processadas imediatamente. No entanto, a curva de fluxo pode não ser suave o suficiente e pode haver um "fenômeno de estímulo" e uma solicitação para o dobro do fluxo limite pode ser gerada quando a janela é alternada. O algoritmo de janela deslizante de contador, como uma melhoria do algoritmo de janela fixa de contador, resolve efetivamente o problema de duas vezes a solicitação de fluxo de limite quando a janela é alternada.

O algoritmo de funil pode retificar o tráfego, permitindo que o tráfego aleatório e instável flua a uma taxa fixa, mas não pode resolver o problema do tráfego repentino . Como uma melhoria do algoritmo de funil, o algoritmo de token bucket não só pode suavizar o fluxo, mas também permite um certo grau de estouro de fluxo.

Os quatro algoritmos de limitação de corrente acima têm suas próprias características e devem ser selecionados de acordo com seus próprios cenários ao utilizá- los.Não existe o melhor algoritmo, apenas o algoritmo mais adequado . Por exemplo, o algoritmo de token bucket geralmente é usado para proteger seu próprio sistema, limitar o fluxo do chamador e proteger seu próprio sistema de tráfego intermitente. Se a capacidade de processamento real do próprio sistema for mais forte do que o limite de fluxo configurado, um certo grau de estouro de tráfego pode ser permitido, de modo que a taxa de processamento real seja maior do que a taxa configurada e os recursos do sistema sejam totalmente utilizados. O algoritmo de funil é geralmente usado para proteger sistemas de terceiros. Por exemplo, seu próprio sistema precisa chamar uma interface de terceiros. Para proteger o sistema de terceiros de ser sobrecarregado por suas próprias chamadas, o algoritmo de funil pode ser usado para limitar o fluxo e garantir que seu próprio fluxo seja estável. Para a interface de terceiros.

O algoritmo está morto e vale a pena aprender a essência do algoritmo . Em cenários reais, ele pode ser usado de forma flexível. Novamente, não existe o melhor algoritmo, apenas o algoritmo mais adequado .

Acho que você gosta

Origin blog.csdn.net/qq_46388795/article/details/108493102
Recomendado
Clasificación