[Microservicios] Diseño e implementación del esquema de limitación de corriente general de Springboot

Tabla de contenido

1. Antecedentes

2. Descripción general de la limitación de corriente

2.1 modelo de gobierno del servicio dubbo

2.1.1 Limitación de corriente a nivel de cuadro dubbo

2.1.2 Configuración del grupo de subprocesos

2.1.3 Integrar componentes de terceros

2.2 modelo de gobierno de servicios de springcloud

2.2.1 histrix

2.2.2 centinela

2.3 Limitación de corriente de la capa de puerta de enlace

3. Estrategias comunes de limitación de corriente

3.1 Algoritmos comúnmente utilizados para la limitación de corriente

3.1.1 Algoritmo de depósito de fichas

3.1.2 Algoritmo de cubeta con fugas

3.1.3 Ventana de tiempo móvil

4. Esquema general de implementación de limitación de corriente

4.1 Realización basada en limitación de corriente de guayaba

4.1.1 Introducción a las dependencias de guayaba

4.1.2 Anotación de límite de corriente personalizada

4.1.3 Clase AOP de limitación de corriente

4.1.4 Interfaz de prueba

4.2 Realización basada en limitación de corriente centinela

4.2.1 Presentación del paquete de dependencia central de Sentinel

4.2.2 Anotación de límite de corriente personalizada

4.2.3 Clase AOP personalizada para implementar la limitación de corriente

4.2.4 Interfaz de prueba personalizada

4.3 Implementación de limitación de corriente basada en redis+lua

4.3.1 Introducir la dependencia de redis

4.3.2 Anotaciones personalizadas

4.3.3 Clase de configuración de redis personalizada

4.3.4 Clase AOP de limitación de corriente personalizada

4.3.5 Personalizar secuencia de comandos lua

4.3.6 Agregar interfaz de prueba

5. Implementación de limitación de corriente de arranque personalizado

5.1 Preparación previa

5.2 Pasos para completar la integración del código

5.2.1 Importar dependencias básicas

5.2.2 Anotaciones personalizadas

5.2.3 Implementación AOP de limitación de corriente

5.2.4 Configuración de la implementación de AOP de ensamblaje automático

5.2.5 Convierta el proyecto en un frasco para la instalación

5.2.6 Introducir el SDK anterior en otros proyectos

5.2.7 Interfaz de prueba de escritura

5.2.8 Prueba de funcionamiento

Sexto, escribe al final del texto


1. Antecedentes

La limitación actual es muy importante para un sistema de arquitectura de microservicios, de lo contrario, uno de los microservicios se convertirá en un factor de avalancha oculto para todo el sistema, ¿por qué dice eso? Por ejemplo, una determinada plataforma SAAS tiene más de 100 aplicaciones de microservicios, pero todas las aplicaciones de nivel superior invocarán con frecuencia una o varias de las aplicaciones subyacentes. la aplicación está obligada a enfrentar una presión tremenda, especialmente para aquellas interfaces que se llaman con frecuencia, el rendimiento más directo es que las nuevas solicitudes entrantes posteriores se bloquean, se ponen en cola y la respuesta se agota... Finalmente, hasta el servicio Donde se agotan los recursos de JVM .

2. Descripción general de la limitación de corriente

Al comienzo del diseño de la mayoría de las arquitecturas de microservicios, como en la etapa de selección de tecnología, los arquitectos planificarán la combinación de pilas de tecnología desde una perspectiva global. Por ejemplo, considerando el status quo de los productos actuales, ¿deberíamos usar dubbo? ¿O nube de primavera? Como el marco subyacente de la gobernanza de microservicios. Incluso para cumplir con el lanzamiento, la iteración y la entrega rápidos, el desarrollo se basa directamente en Springboot, y más adelante se introduce una nueva pila de tecnología...

Por lo tanto, cuando hablamos de soluciones técnicas específicas para un determinado escenario comercial, no debemos generalizarlas, sino que debemos evaluar de manera integral el status quo de los productos y servicios. En términos de limitación de corriente, puede que no sea lo mismo al elegir bajo las siguientes arquitecturas técnicas diferentes.

2.1 modelo de gobierno del servicio dubbo

Elegir el marco dubbo como el gobierno básico del servicio es bueno para las aplicaciones que están sesgadas hacia las plataformas internas. Dubbo usa netty en la capa inferior. En comparación con el protocolo http, todavía tiene ventajas en ciertos escenarios. Si elige dubbo, debe elegir limitación de corriente Las siguientes referencias se pueden hacer en el plano.

2.1.1 Limitación de corriente a nivel de cuadro dubbo

Dubbo proporciona oficialmente una gestión de servicios integral, que puede satisfacer las necesidades de la mayoría de los escenarios de desarrollo. Para el escenario actual limitante, incluye específicamente los siguientes métodos. Para una configuración específica, puede consultar el manual oficial;

  • Limitación de corriente del cliente 
    Limitación de corriente del semáforo (por medio de estadísticas) 
    Limitación de corriente del número de conexión (socket->tcp)
  • Limitación de corriente del lado del servidor 
    Limitación de corriente de grupo de subprocesos (medios de aislamiento) 
    Limitación de corriente de semáforo (medios de no aislamiento) 
    Limitación de corriente de número de recepción (socket->tcp)

2.1.2 Configuración del grupo de subprocesos

Las operaciones concurrentes de subprocesos múltiples deben ser inseparables del grupo de subprocesos. Dubbo en sí proporciona soporte para cuatro tipos de grupos de subprocesos. Los parámetros clave del grupo de subprocesos se pueden configurar en la pestaña del productor <dubbo:protocol>, como el tipo de grupo de subprocesos, el tamaño de la cola de bloqueo y la cantidad de subprocesos principales.Al configurar el número de grupos de subprocesos en el lado de producción, el actual efecto limitante se puede lograr hasta cierto punto.

2.1.3 Integrar componentes de terceros

Si se trata de un proyecto de marco springboot, puede considerar la introducción directa de componentes locales o SDK, como hystrix, guava, sentinel native SDK, etc. Si la fuerza técnica es lo suficientemente fuerte, incluso puede considerar construir sus propias ruedas.

2.2 modelo de gobierno de servicios de springcloud

Si usa springcloud o springcloud-alibaba como su marco de gobernanza de servicios, la propia ecología del marco ya contiene los componentes de limitación de corriente correspondientes, que se pueden usar de forma inmediata.Aquí hay algunos componentes de limitación de corriente de uso común basados ​​en el marco de springcloud. .

2.2.1 histrix

Hystrix es un marco tolerante a fallas de código abierto de Netflix. Cuando springcloud se lanzó al mercado en la etapa inicial, se usó como un componente en el ecosistema springcloud para limitar, fusionar y degradar la corriente. Hystrix proporciona la función de limitación de corriente. En el sistema de arquitectura springcloud, Hystrix se puede habilitar en la puerta de enlace para el procesamiento de limitación de corriente, y cada microservicio también puede habilitar a Hystrix para la limitación de corriente.

Hystrix utiliza el modo de aislamiento de subprocesos de forma predeterminada y puede limitar la corriente a través del número de subprocesos + el tamaño de la cola. Para la configuración de parámetros específicos, consulte la información relevante en el sitio web oficial.

2.2.2 centinela

Sentinel, conocido como el protector de tráfico del sistema distribuido, es un componente importante en la ecología springcloud-alibaba. Es un componente de control de tráfico para la arquitectura de servicios distribuidos. , protección de puntos de acceso y otras dimensiones para ayudar a los desarrolladores a garantizar la estabilidad de los microservicios.

2.3 Limitación de corriente de la capa de puerta de enlace

A medida que aumenta la escala de los microservicios, cuando muchos microservicios en todo el sistema necesitan implementar una limitación de corriente, puede considerar limitar la corriente en la capa de puerta de enlace. En términos generales, la limitación de corriente en la capa de puerta de enlace es para negocios generales, como esas solicitudes maliciosas. , rastreadores, ataques, etc. En términos simples, la limitación del tráfico en el nivel de la puerta de enlace proporciona una capa de protección para el sistema en su conjunto.

3. Estrategias comunes de limitación de corriente

3.1 Algoritmos comúnmente utilizados para la limitación de corriente

Independientemente del tipo de componentes de limitación de corriente, los algoritmos de implementación de limitación de corriente subyacentes son similares. Aquí hay algunos algoritmos de limitación de corriente de uso común para su comprensión.

3.1.1  Algoritmo de depósito de fichas

El algoritmo token bucket es actualmente el algoritmo limitador de corriente más utilizado y, como su nombre lo indica, tiene las siguientes dos funciones clave:

  • Token  : las solicitudes que obtienen tokens se procesarán y otras solicitudes se pondrán en cola o se descartarán directamente;
  • Cubo  : el lugar utilizado para guardar tokens, todas las solicitudes obtienen tokens de este cubo

è¿éæå¥å¾çæè¿°

 El cubo de fichas implica principalmente dos procesos, a saber, la generación de fichas y la adquisición de fichas.

3.1.2  Algoritmo de cubeta con fugas

La primera mitad del algoritmo del balde con fugas es similar al del balde con fichas, pero los objetos de la operación son diferentes, lo que se puede entender en conjunto con la siguiente figura.

El cubo de fichas es para poner el token en el cubo, y el cubo con fugas es para poner el paquete de datos de la solicitud de acceso en el cubo. De manera similar, si el cubo está lleno, se descartarán los nuevos paquetes entrantes.

è¿éæå¥å¾çæè¿°

3.1.3  Ventana de tiempo móvil

De acuerdo con la figura a continuación, describa brevemente el proceso de deslizamiento de la ventana de tiempo:

  • El marco negro grande es la ventana de tiempo, y la unidad de tiempo de la ventana se puede configurar en 5 segundos, y se deslizará hacia atrás a medida que pase el tiempo. Dividimos el tiempo en la ventana en cinco cuadrículas pequeñas, cada cuadrícula representa 1 segundo, y esta cuadrícula también contiene un contador para calcular la cantidad de solicitudes a las que se accedió dentro del tiempo actual. Entonces, el número total de visitas en esta ventana de tiempo es el valor acumulado de todos los contadores de cuadrícula;
  • Por ejemplo, si tenemos 5 visitas de usuarios cada segundo y 10 visitas de usuarios en el quinto segundo, entonces el número de visitas en la ventana de tiempo de 0 a 5 segundos es 15. Si nuestra interfaz establece el límite superior de visitas en la ventana de tiempo en 20, cuando el tiempo llegue al sexto segundo, la suma de los conteos en esta ventana de tiempo será 10, porque la cuadrícula de 1 segundo ha salido de la ventana de tiempo, entonces en El número de visitas que se pueden recibir en el sexto segundo es 20-10=10;

è¿éæå¥å¾çæè¿°

 La ventana deslizante es en realidad un algoritmo de calculadora. Tiene una característica notable. Cuanto más larga sea la ventana de tiempo, más suave será el efecto de limitación actual. Por ejemplo, si la ventana de tiempo actual es de solo dos segundos y todas las solicitudes de acceso se concentran en el primer segundo, cuando el tiempo retrocede un segundo, la cantidad de conteos en la ventana actual cambiará mucho. reducir la posibilidad de que esto suceda

4. Esquema general de implementación de limitación de corriente

Dejando de lado la limitación actual en la capa de puerta de enlace, en las aplicaciones de microservicios, teniendo en cuenta factores como la combinación de pilas de tecnología, el nivel de desarrollo de los miembros del equipo y la facilidad de mantenimiento, un enfoque más común es usar tecnología AOP + personalizado. La anotación implementa limitación de corriente para un método o interfaz específicos.Basado en esta idea, a continuación se presenta la implementación de varios esquemas de limitación de corriente de uso común.

4.1 Realización basada en limitación de corriente de guayaba

Guava es un componente relativamente práctico de código abierto de Google. El uso de este componente puede ayudar a los desarrolladores a completar las operaciones de limitación de corriente convencionales. A continuación, veremos los pasos de implementación específicos.

4.1.1 Introducción a las dependencias de guayaba

La versión puede elegir una versión superior u otra


    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>23.0</version>
    </dependency>

4.1.2 Anotación de límite de corriente personalizada

Personalice una anotación para la limitación actual, y luego solo necesita agregar esta anotación en el método o interfaz que necesita la limitación actual;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface RateConfigAnno {

    String limitType();

    double limitCount() default 5d;
}

4.1.3 Clase AOP de limitación de corriente

Intercepte el método de agregar la anotación de limitación de corriente personalizada mencionada anteriormente a través de la notificación previa de AOP, analice el valor del atributo en la anotación y use el valor del atributo como el parámetro de limitación de corriente proporcionado por guava.Esta clase es el núcleo de la implementación completa.

import com.alibaba.fastjson2.JSONObject;
import com.google.common.util.concurrent.RateLimiter;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Objects;

@Aspect
@Component
public class GuavaLimitAop {

    private static Logger logger = LoggerFactory.getLogger(GuavaLimitAop.class);

    @Before("execution(@RateConfigAnno * *(..))")
    public void limit(JoinPoint joinPoint) {
        //1、获取当前的调用方法
        Method currentMethod = getCurrentMethod(joinPoint);
        if (Objects.isNull(currentMethod)) {
            return;
        }
        //2、从方法注解定义上获取限流的类型
        String limitType = currentMethod.getAnnotation(RateConfigAnno.class).limitType();
        double limitCount = currentMethod.getAnnotation(RateConfigAnno.class).limitCount();
        //使用guava的令牌桶算法获取一个令牌,获取不到先等待
        RateLimiter rateLimiter = RateLimitHelper.getRateLimiter(limitType, limitCount);
        boolean b = rateLimiter.tryAcquire();
        if (b) {
            System.out.println("获取到令牌");
        }else {
            HttpServletResponse resp = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
            JSONObject jsonObject=new JSONObject();
            jsonObject.put("success",false);
            jsonObject.put("msg","限流中");
            try {
                output(resp, jsonObject.toJSONString());
            }catch (Exception e){
                logger.error("error,e:{}",e);
            }
        }
    }

    private Method getCurrentMethod(JoinPoint joinPoint) {
        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        Method target = null;
        for (Method method : methods) {
            if (method.getName().equals(joinPoint.getSignature().getName())) {
                target = method;
                break;
            }
        }
        return target;
    }

    public void output(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(msg.getBytes("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            outputStream.flush();
            outputStream.close();
        }
    }
}

Entre ellos, la API central de limitación de corriente es el objeto de RateLimiter, y la clase RateLimitHelper relacionada es la siguiente

import com.google.common.util.concurrent.RateLimiter;

import java.util.HashMap;
import java.util.Map;

public class RateLimitHelper {

    private RateLimitHelper(){}

    private static Map<String,RateLimiter> rateMap = new HashMap<>();

    public static RateLimiter getRateLimiter(String limitType,double limitCount ){
        RateLimiter rateLimiter = rateMap.get(limitType);
        if(rateLimiter == null){
            rateLimiter = RateLimiter.create(limitCount);
            rateMap.put(limitType,rateLimiter);
        }
        return rateLimiter;
    }

}

4.1.4 Interfaz de prueba

Agregue una interfaz de prueba a continuación para probar si el código anterior funciona

@RestController
public class OrderController {

    //localhost:8081/save
    @GetMapping("/save")
    @RateConfigAnno(limitType = "saveOrder",limitCount = 1)
    public String save(){
        return "success";
    }

}

Para simular el efecto en la interfaz, configuramos los parámetros muy pequeños, es decir, QPS es 1. Se puede esperar que cuando la solicitud por segundo exceda 1, habrá un aviso de limitación actual. Inicie el proyecto y verifique la interfaz, una vez por segundo Solicitud, el resultado se puede obtener normalmente, el efecto es el siguiente:

Cepille rápidamente la interfaz, verá el siguiente efecto

4.2 Realización basada en limitación de corriente centinela

En la conciencia de muchos estudiantes, Sentinel generalmente debe combinarse con el marco springcloud-alibaba para que sea práctico y, después de integrarse con el marco, se puede usar con la consola para lograr mejores resultados. native El SDK está disponible y la integración se realiza de esta manera.

4.2.1 Presentación del paquete de dependencia central de Sentinel

    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-core</artifactId>
        <version>1.8.0</version>
    </dependency>

4.2.2 Anotación de límite de corriente personalizada

Puede agregar más atributos según sea necesario

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface SentinelLimitAnnotation {

    String resourceName();

    int limitCount() default 5;

}

4.2.3 Clase AOP personalizada para implementar la limitación de corriente

La idea de implementación de esta clase es similar al uso de guayaba mencionado anteriormente, la diferencia es que aquí se usa la API relacionada con la limitación de corriente de Sentinel original, y si la propiedad no es suficiente, puede consultar el documento oficial para aprender, así que no lo extenderé aquí.


import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@Aspect
@Component
public class SentinelMethodLimitAop {

    private static void initFlowRule(String resourceName,int limitCount) {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        //设置受保护的资源
        rule.setResource(resourceName);
        //设置流控规则 QPS
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        //设置受保护的资源阈值
        rule.setCount(limitCount);
        rules.add(rule);
        //加载配置好的规则
        FlowRuleManager.loadRules(rules);
    }

    @Pointcut(value = "@annotation(com.congge.sentinel.SentinelLimitAnnotation)")
    public void rateLimit() {

    }

    @Around("rateLimit()")
    public Object around(ProceedingJoinPoint joinPoint) {
        //1、获取当前的调用方法
        Method currentMethod = getCurrentMethod(joinPoint);
        if (Objects.isNull(currentMethod)) {
            return null;
        }
        //2、从方法注解定义上获取限流的类型
        String resourceName = currentMethod.getAnnotation(SentinelLimitAnnotation.class).resourceName();
        if(StringUtils.isEmpty(resourceName)){
            throw new RuntimeException("资源名称为空");
        }
        int limitCount = currentMethod.getAnnotation(SentinelLimitAnnotation.class).limitCount();
        initFlowRule(resourceName,limitCount);

        Entry entry = null;
        Object result = null;
        try {
            entry = SphU.entry(resourceName);
            try {
                result = joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        } catch (BlockException ex) {
            // 资源访问阻止,被限流或被降级
            // 在此处进行相应的处理操作
            System.out.println("blocked");
            return "被限流了";
        } catch (Exception e) {
            Tracer.traceEntry(e, entry);
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
        return result;
    }

    private Method getCurrentMethod(JoinPoint joinPoint) {
        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        Method target = null;
        for (Method method : methods) {
            if (method.getName().equals(joinPoint.getSignature().getName())) {
                target = method;
                break;
            }
        }
        return target;
    }
}

4.2.4 Interfaz de prueba personalizada

Para simular el efecto, el número de QPS se establece en 1 aquí

    //localhost:8081/limit
    @GetMapping("/limit")
    @SentinelLimitAnnotation(limitCount = 1,resourceName = "sentinelLimit")
    public String sentinelLimit(){
        return "sentinelLimit";
    }

Después de iniciar el proyecto, el navegador llama a la interfaz para probar, una solicitud por segundo puede pasar normalmente

Actualice rápidamente la interfaz, cuando exceda 1 vez por segundo, el efecto es el siguiente

Esto es solo para demostrar el efecto. Se recomienda encapsular el resultado devuelto cuando se usa en un proyecto real.

4.3 Implementación de limitación de corriente basada en redis+lua

Redis es seguro para subprocesos , naturalmente tiene características seguras para subprocesos y admite operaciones atómicas . Utilice estas funciones de Redis para limitar la corriente, lo que puede garantizar tanto la seguridad como el rendimiento de los subprocesos. El flujo completo de implementación de limitación de corriente basada en redis es el siguiente:

è¿éæå¥å¾çæè¿°

Combinado con el diagrama de flujo anterior, aquí hay una idea general de implementación:

  • Escriba un script lua para especificar las reglas de limitación actuales para los parámetros de entrada. Por ejemplo, al limitar la corriente de una interfaz específica, puede hacer un juicio basado en uno o varios parámetros, llamar a la solicitud de la interfaz y monitorear el número de solicitudes dentro de una determinada ventana de tiempo;
  • Dado que es una limitación de corriente, es mejor que sea universal, y las reglas de limitación de corriente se pueden aplicar a cualquier interfaz, por lo que la forma más adecuada es cortar a través de anotaciones personalizadas;
  • Proporcione una clase de configuración, que es administrada por el contenedor Spring, y el bean DefaultRedisScript se proporciona en redisTemplate;
  • Proporcione una clase que pueda analizar dinámicamente los parámetros de la interfaz y activar la limitación actual después de hacer coincidir las reglas de acuerdo con los parámetros de la interfaz;

4.3.1 Introducir la dependencia de redis

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

4.3.2 Anotaciones personalizadas

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisLimitAnnotation {

    /**
     * key
     */
    String key() default "";
    /**
     * Key的前缀
     */
    String prefix() default "";
    /**
     * 一定时间内最多访问次数
     */
    int count();
    /**
     * 给定的时间范围 单位(秒)
     */
    int period();
    /**
     * 限流的类型(用户自定义key或者请求ip)
     */
    LimitType limitType() default LimitType.CUSTOMER;

}

4.3.3 Clase de configuración de redis personalizada

import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;

import java.io.Serializable;

@Component
public class RedisConfiguration {

    @Bean
    public DefaultRedisScript<Number> redisluaScript() {
        DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
        redisScript.setResultType(Number.class);
        return redisScript;
    }

    @Bean("redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        //设置value的序列化方式为JSOn
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        //设置key的序列化方式为String
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

}

4.3.4 Clase AOP de limitación de corriente personalizada

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;

@Aspect
@Configuration
public class LimitRestAspect {

    private static final Logger logger = LoggerFactory.getLogger(LimitRestAspect.class);

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private DefaultRedisScript<Number> redisluaScript;


    @Pointcut(value = "@annotation(com.congge.config.limit.RedisLimitAnnotation)")
    public void rateLimit() {

    }

    @Around("rateLimit()")
    public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        RedisLimitAnnotation rateLimit = method.getAnnotation(RedisLimitAnnotation.class);
        if (rateLimit != null) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String ipAddress = getIpAddr(request);
            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append(ipAddress).append("-")
                    .append(targetClass.getName()).append("- ")
                    .append(method.getName()).append("-")
                    .append(rateLimit.key());
            List<String> keys = Collections.singletonList(stringBuffer.toString());
            //调用lua脚本,获取返回结果,这里即为请求的次数
            Number number = redisTemplate.execute(
                    redisluaScript,
                    keys,
                    rateLimit.count(),
                    rateLimit.period()
            );
            if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
                logger.info("限流时间段内访问了第:{} 次", number.toString());
                return joinPoint.proceed();
            }
        } else {
            return joinPoint.proceed();
        }
        throw new RuntimeException("访问频率过快,被限流了");
    }

    /**
     * 获取请求的IP方法
     * @param request
     * @return
     */
    private static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) {
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }

}

Lo que esta clase tiene que hacer es similar a las dos medidas de limitación de corriente anteriores, pero aquí la limitación de corriente central se realiza leyendo el script lua y pasando parámetros al script lua.

4.3.5 Personalizar secuencia de comandos lua

En el directorio de recursos del proyecto, agregue el siguiente script lua



local key = "rate.limit:" .. KEYS[1]

local limit = tonumber(ARGV[1])

local current = tonumber(redis.call('get', key) or "0")

if current + 1 > limit then
  return 0
else
   -- 没有超阈值,将当前访问数量+1,并设置2秒过期(可根据自己的业务情况调整)
   redis.call("INCRBY", key,"1")
   redis.call("expire", key,"2")
   return current + 1
end

4.3.6 Agregar interfaz de prueba

@RestController
public class RedisController {

    //localhost:8081/redis/limit
    @GetMapping("/redis/limit")
    @RedisLimitAnnotation(key = "queryFromRedis",period = 1, count = 1)
    public String queryFromRedis(){
        return "success";
    }

}

Para simular el efecto, establezca aquí QPS en 1. Después de iniciar el proyecto (iniciar el servicio Redis por adelantado), llame a la interfaz. El efecto normal es el siguiente:

Actualice rápidamente la interfaz y vea el siguiente efecto cuando haya más de 1 solicitud por segundo

5. Implementación de limitación de corriente de arranque personalizado

Lo anterior presenta varias implementaciones de limitación de corriente de uso común a través de casos, pero los estudiantes atentos pueden ver que estas implementaciones de limitación de corriente están integradas en módulos de ingeniería específicos. De hecho, en el desarrollo real de microservicios, un proyecto puede contener muchos módulos de microservicios. Para reducir la creación repetida de ruedas y evitar una implementación separada en cada módulo de microservicio, puede considerar encapsular la implementación lógica de limitación de corriente en un SDK, es decir, como un iniciador Springboot para ser utilizado por otros módulos de microservicios a los que se puede hacer referencia. Esta es también una práctica relativamente común en muchas prácticas de producción en la actualidad. Echemos un vistazo a la implementación específica a continuación.

5.1 Preparación previa

Cree un proyecto springboot vacío. La estructura del directorio del proyecto es como se muestra en la figura a continuación. La descripción del directorio:

  • anotación: almacene anotaciones personalizadas relacionadas con la limitación actual;
  • aop: almacena diferentes implementaciones de limitación de corriente, como aop basado en guayaba, implementación de aop basado en centinela, etc.;
  • spring.factories: personaliza la clase de implementación de aop que se ensamblará;

5.2 Pasos para completar la integración del código

5.2.1 Importar dependencias básicas

Esto incluye las siguientes dependencias necesarias, y otras dependencias pueden seleccionarse razonablemente según su propia situación;

  • resorte-bota-arrancador;
  • guayaba;
  • spring-boot-autoconfigure;
  • centinela-núcleo;
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- guava-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-core</artifactId>
            <version>1.8.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.22</version>
        </dependency>

    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/**</include>
                </includes>
            </resource>
        </resources>
    </build>

5.2.2 Anotaciones personalizadas

En la actualidad, el SDK admite tres métodos de limitación de corriente, es decir, en otros proyectos de microservicios posteriores, la limitación de corriente se puede realizar mediante la adición de estos tres tipos de anotaciones, que son cubo de fichas basado en guayaba, limitación de corriente basada en centinela y basada en en la limitación de corriente del propio semáforo de Java, tres clases de anotación personalizadas son las siguientes:

cubo de fichas

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)

public @interface TokenBucketLimiter {
    int value() default 50;
}

Semáforo

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ShLimiter {
    int value() default 50;
}

centinela

@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface SentinelLimiter {

    String resourceName();

    int limitCount() default 50;

}

5.2.3 Implementación AOP de limitación de corriente

La limitación de corriente específica se implementa en AOP, y la idea es similar al capítulo anterior, es decir, a través de la forma de notificaciones circundantes, primero analice los métodos con anotaciones de limitación de corriente agregadas y luego analice los parámetros internos para realizar la limitación de corriente. negocio.

Implementación de aop basado en guayaba

import com.alibaba.fastjson2.JSONObject;
import com.congge.annotation.TokenBucketLimiter;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.cglib.core.ReflectUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Aspect
@Component
@Slf4j
public class GuavaLimiterAop {

    private final Map<String, RateLimiter> rateLimiters = new ConcurrentHashMap<String, RateLimiter>();

    @Pointcut("@annotation(com.congge.annotation.TokenBucketLimiter)")
    public void aspect() {
    }

    @Around(value = "aspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        log.debug("准备限流");
        Object target = point.getTarget();
        String targetName = target.getClass().getName();
        String methodName = point.getSignature().getName();
        Object[] arguments = point.getArgs();
        Class<?> targetClass = Class.forName(targetName);
        Class<?>[] argTypes = ReflectUtils.getClasses(arguments);
        Method method = targetClass.getDeclaredMethod(methodName, argTypes);
        // 获取目标method上的限流注解@Limiter
        TokenBucketLimiter limiter = method.getAnnotation(TokenBucketLimiter.class);
        RateLimiter rateLimiter = null;
        Object result = null;
        if (null != limiter) {
            // 以 class + method + parameters为key,避免重载、重写带来的混乱
            String key = targetName + "." + methodName + Arrays.toString(argTypes);
            rateLimiter = rateLimiters.get(key);
            if (null == rateLimiter) {
                // 获取限定的流量
                // 为了防止并发
                rateLimiters.putIfAbsent(key, RateLimiter.create(limiter.value()));
                rateLimiter = rateLimiters.get(key);
            }
            boolean b = rateLimiter.tryAcquire();
            if(b){
                log.debug("得到令牌,准备执行业务");
                result = point.proceed();
            }else {
                HttpServletResponse resp = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
                JSONObject jsonObject=new JSONObject();
                jsonObject.put("success",false);
                jsonObject.put("msg","限流中");
                try {
                    output(resp, jsonObject.toJSONString());
                }catch (Exception e){
                    log.error("error,e:{}",e);
                }
            }
        } else {
            result = point.proceed();
        }
        log.debug("退出限流");
        return result;
    }

    public void output(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(msg.getBytes("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            outputStream.flush();
            outputStream.close();
        }
    }
}

Implementación de Aop basada en Semaphore

import com.congge.annotation.ShLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cglib.core.ReflectUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;

@Aspect
@Component
@Slf4j
public class SemaphoreLimiterAop {

    private final Map<String, Semaphore> semaphores = new ConcurrentHashMap<String, Semaphore>();
    private final static Logger LOG = LoggerFactory.getLogger(SemaphoreLimiterAop.class);

    @Pointcut("@annotation(com.congge.annotation.ShLimiter)")
    public void aspect() {

    }

    @Around(value = "aspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        log.debug("进入限流aop");
        Object target = point.getTarget();
        String targetName = target.getClass().getName();
        String methodName = point.getSignature().getName();
        Object[] arguments = point.getArgs();
        Class<?> targetClass = Class.forName(targetName);
        Class<?>[] argTypes = ReflectUtils.getClasses(arguments);
        Method method = targetClass.getDeclaredMethod(methodName, argTypes);
        // 获取目标method上的限流注解@Limiter
        ShLimiter limiter = method.getAnnotation(ShLimiter.class);
        Object result = null;
        if (null != limiter) {
            // 以 class + method + parameters为key,避免重载、重写带来的混乱
            String key = targetName + "." + methodName + Arrays.toString(argTypes);
            // 获取限定的流量
            Semaphore semaphore = semaphores.get(key);
            if (null == semaphore) {
                semaphores.putIfAbsent(key, new Semaphore(limiter.value()));
                semaphore = semaphores.get(key);
            }
            try {
                semaphore.acquire();
                result = point.proceed();
            } finally {
                if (null != semaphore) {
                    semaphore.release();
                }
            }
        } else {
            result = point.proceed();
        }
        log.debug("退出限流");
        return result;
    }

}

Implementación AOP basada en centinela

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.congge.annotation.SentinelLimiter;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

@Aspect
@Component
public class SentinelLimiterAop {

    private static void initFlowRule(String resourceName,int limitCount) {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        //设置受保护的资源
        rule.setResource(resourceName);
        //设置流控规则 QPS
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        //设置受保护的资源阈值
        rule.setCount(limitCount);
        rules.add(rule);
        //加载配置好的规则
        FlowRuleManager.loadRules(rules);
    }

    @Pointcut(value = "@annotation(com.congge.annotation.SentinelLimiter)")
    public void rateLimit() {

    }

    @Around("rateLimit()")
    public Object around(ProceedingJoinPoint joinPoint) {
        //1、获取当前的调用方法
        Method currentMethod = getCurrentMethod(joinPoint);
        if (Objects.isNull(currentMethod)) {
            return null;
        }
        //2、从方法注解定义上获取限流的类型
        String resourceName = currentMethod.getAnnotation(SentinelLimiter.class).resourceName();
        if(StringUtils.isEmpty(resourceName)){
            throw new RuntimeException("资源名称为空");
        }
        int limitCount = currentMethod.getAnnotation(SentinelLimiter.class).limitCount();
        initFlowRule(resourceName,limitCount);

        Entry entry = null;
        Object result = null;
        try {
            entry = SphU.entry(resourceName);
            try {
                result = joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        } catch (BlockException ex) {
            // 资源访问阻止,被限流或被降级
            // 在此处进行相应的处理操作
            System.out.println("blocked");
            return "被限流了";
        } catch (Exception e) {
            Tracer.traceEntry(e, entry);
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
        return result;
    }

    private Method getCurrentMethod(JoinPoint joinPoint) {
        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        Method target = null;
        for (Method method : methods) {
            if (method.getName().equals(joinPoint.getSignature().getName())) {
                target = method;
                break;
            }
        }
        return target;
    }

}

5.2.4 Configuración de la implementación de AOP de ensamblaje automático

Cree el archivo spring.factories anterior en el directorio de recursos, el contenido es el siguiente, después de la configuración de esta manera, se pueden usar otros módulos de la aplicación después de introducir el jar del SDK actual;

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.congge.aop.SemaphoreLimiterAop,\
  com.congge.aop.GuavaLimiterAop,\
  com.congge.aop.SemaphoreLimiterAop

5.2.5 Convierta el proyecto en un frasco para la instalación

Este paso es más fácil de saltar

5.2.6 Introducir el SDK anterior en otros proyectos

        <dependency>
            <groupId>cm.congge</groupId>
            <artifactId>biz-limit</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

5.2.7 Interfaz de prueba de escritura

En otros proyectos, escriba una interfaz de prueba y use las anotaciones anteriores. Aquí tomamos las anotaciones de limitación de corriente de guayaba como ejemplo para ilustrar

import com.congge.annotation.TokenBucketLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SdkController {

    //localhost:8081/query
    @GetMapping("/query")
    @TokenBucketLimiter(1)
    public String queryUser(){
        return "queryUser";
    }

}

5.2.8 Prueba de funcionamiento

Después de iniciar el proyecto actual, llame a la interfaz normalmente, solicite una vez por segundo y obtenga el resultado normalmente

Actualice rápidamente la interfaz, después de que el QPS exceda 1, se activará el límite actual y se verá el siguiente efecto

A través del método anterior, también se puede obtener el efecto esperado. Los estudiantes que estén interesados ​​en las otras dos anotaciones de limitación de corriente también pueden continuar probando y verificando, y la razón del espacio no se repetirá.

El método de inicio mencionado anteriormente implementa un método de integración de limitación de corriente más elegante, que también es un método recomendado en producción, pero el caso actual aún es relativamente difícil, y los estudiantes que necesitan usarlo deben mejorar la lógica de acuerdo con su propia situación Encapsulación adicional para obtener mejores resultados.

Sexto, escribe al final del texto

Este documento elabora algunos esquemas de implementación de la limitación actual en microservicios en un espacio grande combinado con casos reales. La limitación actual es de gran importancia para un sistema operativo estable. Se puede decir que es un aspecto importante de la gobernanza del servicio. Espero que haya sido útil para los estudiantes que lo vieron, gracias por mirar.

Supongo que te gusta

Origin blog.csdn.net/zhangcongyi420/article/details/131342759
Recomendado
Clasificación