Este artículo comprende el algoritmo limitante actual de las preguntas de entrevistas de alta frecuencia, desde el principio del algoritmo hasta la implementación y luego al análisis comparativo.

Lectura recomendada:

Maldita sea, es un vaquero, los programadores han estado trabajando duro en estos algoritmos durante 47 días y los bytes de cuatro lados ganaron la oferta.

¡Adoración! El libro de algoritmos de nivel maestro de 666 páginas resumido por Byte Great God, elimina a LeetCode en minutos

Algoritmo de ventana fija de contador

principio

El algoritmo de ventana fija de contador es el algoritmo de limitación de corriente más básico y simple. El principio es contar las solicitudes dentro de una ventana de tiempo fija. Si el número de solicitudes excede el umbral, la solicitud se descarta; si no se alcanza el umbral establecido, la solicitud se acepta y el recuento se incrementa en 1. Cuando finalice la ventana de tiempo, restablezca el contador a 0.

Diagrama esquemático del algoritmo de ventana fija de contador

Implementación y prueba de código

También es relativamente sencillo de implementar, como sigue:

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));
    }

}

Los resultados de la prueba son los siguientes:

Contador de resultados de prueba de algoritmo de ventana fija

Se puede observar que solo 20 de las 50 solicitudes aprobadas y 30 fueron restringidas, lo que logró el efecto de restricción actual esperado.

Análisis característico

Ventajas : simple de implementar y fácil de entender.

Desventajas : Es posible que la curva de flujo no sea lo suficientemente suave, con "fenómeno de estimulación", como se muestra en la figura siguiente. Habrá dos problemas:

Contador de la curva de límite de corriente del algoritmo de ventana fija

  1. El servicio del sistema no está disponible por un período de tiempo (sin exceder la ventana de tiempo) . Por ejemplo, el tamaño de la ventana es 1 s, el tamaño límite actual es 100, y luego ocurren 100 solicitudes en los primeros ms de una ventana, y luego se rechazarán las solicitudes de 2 ms a 999 ms. Durante este tiempo, los usuarios sentirán que el servicio del sistema no está disponible.

  2. Cuando se cambia la ventana, se puede generar una solicitud del doble del tráfico de umbral . Por ejemplo, el tamaño de la ventana es 1 s, el tamaño límite actual es 100 y luego llegan 100 solicitudes en los 999 ms de una determinada ventana. No hay solicitudes en la etapa inicial de la ventana, por lo que las 100 solicitudes pasarán. Da la casualidad de que 100 solicitudes llegaron en los primeros ms de la siguiente ventana, y todas pasaron, es decir, 200 solicitudes pasaron en 2 ms, y el umbral que establecimos fue 100, y las solicitudes pasadas alcanzaron el umbral. doble.

    El algoritmo de limitación de corriente de la ventana fija del contador genera solicitudes para el doble del flujo de umbral

Algoritmo de ventana deslizante de contador

principio

El algoritmo de la ventana deslizante del contador es una mejora del algoritmo de la ventana fija del contador, que resuelve el inconveniente del doble de la solicitud de tráfico de umbral cuando se cambia la ventana fija.

El algoritmo de ventana deslizante divide una ventana de tiempo en varias ventanas pequeñas sobre la base de una ventana fija, y luego cada ventana pequeña mantiene un contador independiente. Cuando el tiempo solicitado es mayor que el tiempo máximo de la ventana actual, la ventana de tiempo se desplaza hacia adelante en una pequeña ventana. Al hacer una panorámica, descarte los datos de la primera ventana pequeña, luego configure la segunda ventana pequeña como la primera ventana pequeña, agregue una ventana pequeña al final y coloque la nueva solicitud en la ventana pequeña recién agregada . Al mismo tiempo, es necesario asegurarse de que el número de solicitudes para todas las ventanas pequeñas en toda la ventana no pueda exceder el umbral establecido posteriormente.

Diagrama esquemático del algoritmo de ventana deslizante contraria

No es difícil ver en la figura que el algoritmo de la ventana deslizante es una versión mejorada de la ventana fija. Al dividir la ventana de tiempo en una pequeña ventana, el algoritmo de ventana deslizante degenera en un algoritmo de ventana fija. El algoritmo de ventana deslizante en realidad limita el número de solicitudes con una granularidad más fina. Cuanto más ventanas se dividan, más preciso será el límite actual.

Implementación y prueba 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);
    }
}

En la prueba, el tamaño de la ventana deslizante es 1000/10 = 100 ms, y luego se simulan 100 conjuntos de 50 solicitudes con un intervalo de 150 ms. El algoritmo de la ventana deslizante del contador se compara con el algoritmo de la ventana fija del contador y se pueden ver los siguientes resultados:

Resultados de la prueba del algoritmo de la ventana deslizante del contador

El algoritmo de ventana fija genera un problema del doble de la solicitud de tráfico de umbral cuando se cambia la ventana, y el algoritmo de ventana deslizante evita este problema.

Análisis característico

  1. Evite el problema de que el algoritmo de ventana fija del contador puede generar el doble de la solicitud de tráfico de umbral cuando se cambia la ventana fija;
  2. En comparación con el algoritmo del embudo, también se pueden procesar nuevas solicitudes, evitando el problema del hambre del algoritmo del embudo.

Algoritmo de embudo

principio

El principio del algoritmo de embudo también es fácil de entender. Después de que llegue la solicitud, primero ingresará al embudo y luego el embudo procesará el flujo de salida de la solicitud a una velocidad constante, suavizando así el flujo. Cuando el tráfico solicitado es demasiado grande, el embudo se desbordará cuando se alcance la capacidad máxima y la solicitud se descartará en este momento. Desde el punto de vista del sistema, no sabemos cuándo habrá solicitudes y no sabemos qué tan rápido llegarán las solicitudes, lo que crea peligros ocultos para la seguridad del sistema. Pero si agrega una capa de algoritmo de embudo para limitar la corriente, puede asegurarse de que la solicitud fluya a un ritmo constante. Desde el punto de vista del sistema, la solicitud siempre llega a una velocidad de transmisión uniforme, protegiendo así el sistema.

Diagrama esquemático del algoritmo de embudo

Implementación y prueba 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 + "号请求被拒绝");
            }
        }
    }
}

En la prueba, la capacidad del algoritmo de limitación de corriente del embudo es 5 y la velocidad del embudo es 2 por segundo, y luego se simulan 10 solicitudes consecutivas, numeradas del 1 al 10, y los resultados son los siguientes:

Resultados de la prueba del algoritmo de embudo

Se puede ver que las solicitudes 1-5 fueron aceptadas, mientras que las solicitudes 6-10 fueron rechazadas, lo que indica que el embudo se ha desbordado en este momento, lo que está en línea con nuestras expectativas.

Prestemos atención al procesamiento de las cinco solicitudes aceptadas, podemos ver que aunque se aceptan las cinco solicitudes, el procesamiento se procesa una a una (no necesariamente en orden, según la implementación específica), aproximadamente cada 500ms procesando uno. Esto refleja las características del algoritmo de embudo, es decir, aunque el flujo de solicitud se genera instantáneamente, la solicitud fluye a una velocidad fija y se procesa. Debido a que establecemos la velocidad del embudo en 2 por segundo, cada 500 ms el embudo filtrará una solicitud y luego la procesará.

Análisis característico

  1. La tasa de fuga del barril con fugas es fija, lo que puede desempeñar un papel de rectificación . Es decir, si bien el flujo solicitado puede ser aleatorio, grande y pequeño, después del algoritmo del embudo, se convierte en un flujo estable con una tasa fija, que protege el sistema aguas abajo.
  2. No se puede resolver el problema del tráfico repentino . Tomemos el ejemplo que acabamos de probar. Establecimos la velocidad del embudo en 2 por segundo y, de repente, llegaron 10 solicitudes. Debido a la capacidad del embudo, solo se aceptaron 5 solicitudes y las otras 5 se rechazaron. Podría decirse que la velocidad del embudo es de 2 por segundo y luego se aceptan 5 solicitudes instantáneamente. ¿No resuelve esto el problema del tráfico repentino? No, estas 5 solicitudes solo se aceptaron, pero no se procesaron de inmediato. La velocidad de procesamiento sigue siendo de 2 por segundo como establecimos, por lo que el problema de ráfagas de tráfico no está resuelto. Y el algoritmo de cubeta de tokens del que vamos a hablar puede resolver el problema de las ráfagas de tráfico hasta cierto punto. Los lectores pueden compararlo.

Algoritmo de depósito de tokens

principio

El algoritmo de token bucket es una mejora del algoritmo de embudo. Además de limitar el flujo, también permite un cierto grado de ráfaga de tráfico. En el algoritmo del depósito de tokens, hay un depósito de tokens y hay un mecanismo en el algoritmo para colocar tokens en el depósito de tokens a una tasa constante. El depósito de tokens también tiene cierta capacidad y, si está lleno, no se puede colocar el token. Cuando llega una solicitud, primero irá al depósito de tokens para obtener el token. Si se obtiene el token, la solicitud se procesará y el token se consumirá; si el depósito de tokens está vacío, la solicitud se tirado.

Diagrama esquemático del algoritmo de token bucket

Implementación y prueba 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 + "号请求被拒绝");
            }
        }
    }
}

En la prueba, para comparar con el algoritmo de limitación de corriente del embudo, la capacidad del algoritmo del depósito de tokens también es 5, la tasa de generación del token es 2 por segundo, y luego se simulan 10 solicitudes consecutivas, numeradas del 1 al 10, Los resultados son los siguientes:

Resultados de la prueba del algoritmo del depósito de tokens

Se puede ver que para 10 solicitudes, el algoritmo del depósito de tokens es el mismo que el algoritmo del embudo, se aceptan 5 solicitudes y se rechazan 5 solicitudes. A diferencia del algoritmo de embudo, el algoritmo de depósito de tokens procesa estas 5 solicitudes de inmediato, y la velocidad de procesamiento se puede considerar como 5 por segundo, lo que excede la tasa que establecimos 2 por segundo, lo que permite un cierto grado de ráfaga de tráfico. . Esta es también la principal diferencia con el algoritmo de embudo, que se puede experimentar con cuidado.

Análisis característico

El algoritmo de cubeta de tokens es una mejora del algoritmo de cubeta con fugas. No solo puede limitar la tasa promedio de llamadas, sino que también permite cierto grado de ráfaga de tráfico.

resumen

Resumamos brevemente los cuatro algoritmos de limitación de corriente anteriores.

El algoritmo de ventanilla fija de contador es sencillo de implementar y de entender. En comparación con el algoritmo de embudo, las nuevas solicitudes también se pueden procesar de inmediato. Sin embargo, la curva de flujo puede no ser lo suficientemente suave y puede haber un "fenómeno de estímulo", y puede generarse una solicitud del doble del flujo umbral cuando se cambia la ventana. El algoritmo de la ventana deslizante del contador, como una mejora del algoritmo de la ventana fija del contador, resuelve eficazmente el problema del doble de la solicitud de flujo umbral cuando se cambia la ventana.

El algoritmo del embudo puede rectificar el tráfico, permitiendo que el tráfico aleatorio e inestable fluya a una velocidad fija, pero no puede resolver el problema del tráfico repentino . Como una mejora del algoritmo de embudo, el algoritmo de cubeta de tokens no solo puede suavizar el flujo, sino que también permite un cierto grado de ráfaga de flujo.

Los cuatro algoritmos de limitación de corriente anteriores tienen sus propias características, y deben seleccionarse de acuerdo con sus propios escenarios al usarlos. No existe el mejor algoritmo, solo el algoritmo más adecuado . Por ejemplo, el algoritmo de depósito de tokens se utiliza generalmente para proteger su propio sistema, limitar el flujo de la persona que llama y proteger su propio sistema del tráfico en ráfagas. Si la capacidad de procesamiento real del propio sistema es mayor que el límite de flujo configurado, se puede permitir un cierto grado de ráfaga de tráfico, de modo que la tasa de procesamiento real sea mayor que la tasa configurada y los recursos del sistema se utilicen por completo. El algoritmo de embudo se utiliza generalmente para proteger sistemas de terceros. Por ejemplo, su propio sistema necesita llamar a una interfaz de terceros. Para evitar que el sistema de terceros se vea abrumado por sus propias llamadas, el algoritmo de embudo se puede utilizar para limitar el flujo y garantizar que su propio flujo sea estable. A la interfaz de terceros.

El algoritmo está muerto y vale la pena aprender la esencia del algoritmo . En escenarios reales, se puede usar de manera flexible. Nuevamente, no existe el mejor algoritmo, solo el algoritmo más adecuado .

Supongo que te gusta

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