Maison > base de données > Redis > Comment utiliser AOP+redis+lua pour limiter le courant

Comment utiliser AOP+redis+lua pour limiter le courant

WBOY
Libérer: 2023-06-03 17:43:03
avant
543 Les gens l'ont consulté

Exigences

La société utilise la méthode OneByOne pour supprimer des données. Afin d'éviter qu'un trop grand nombre de données ne soient supprimées au cours d'une période donnée, permettez-moi de définir ici une limite de courant d'interface. Lorsqu'elle dépasse un certain seuil, une exception sera créée. sera signalé et l’opération de suppression sera terminée.

Méthode d'implémentation

Créez des annotations personnalisées @limit 让使用者在需要的地方配置 count(一定时间内最多访问次数)period(给定的时间范围),也就是访问频率。然后通过LimitInterceptorInterceptez les requêtes de méthode et contrôlez la fréquence d'accès via le script redis+lua. I Annotation mlimit du code source

La fréquence d'accès de la méthode de configuration le fait, Period

import javax.validation.constraints.Min;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit {
    /**
     * key
     */
    String key() default "";
    /**
     * Key的前缀
     */
    String prefix() default "";
    /**
     * 一定时间内最多访问次数
     */
    @Min(1)
    int count();
    /**
     * 给定的时间范围 单位(秒)
     */
    @Min(1)
    int period();
    /**
     * 限流的类型(用户自定义key或者请求ip)
     */
    LimitType limitType() default LimitType.CUSTOMER;
}
Copier après la connexion

limitkey

est utilisée pour marquer les paramètres, dans le cadre de la clé REDIS

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LimitKey {
}
Copier après la connexion
e

limittype

énumération, le type de la valeur de la clé Redis, prend en charge la clé et l'IP personnalisées, methodName pour obtenir la clé

public enum LimitType {
    /**
     * 自定义key
     */
    CUSTOMER,
    /**
     * 请求者IP
     */
    IP,
    /**
     * 方法名称
     */
    METHOD_NAME;
}
Copier après la connexion

RedisLimiterHelper

Initialiser un bean redisTemplate utilisé pour la limitation actuelle

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
@Configuration
public class RedisLimiterHelper {
    @Bean
    public RedisTemplate<String, Serializable> limitRedisTemplate(@Qualifier("defaultStringRedisTemplate") StringRedisTemplate redisTemplate) {
        RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisTemplate.getConnectionFactory());
        return template;
    }
}
Copier après la connexion

LimitInterceptor

Utiliser la méthode aop pour intercepter la demande, contrôler l'accès fréquence

import com.google.common.collect.ImmutableList;
import com.yxt.qida.api.bean.service.xxv2.openapi.anno.Limit;
import com.yxt.qida.api.bean.service.xxv2.openapi.anno.LimitKey;
import com.yxt.qida.api.bean.service.xxv2.openapi.anno.LimitType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
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.data.redis.core.script.RedisScript;
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.annotation.Annotation;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Configuration
public class LimitInterceptor {
    private static final String UNKNOWN = "unknown";
    private final RedisTemplate<String, Serializable> limitRedisTemplate;
    @Autowired
    public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
        this.limitRedisTemplate = limitRedisTemplate;
    }
    @Around("execution(public * *(..)) && @annotation(com.yxt.qida.api.bean.service.xxv2.openapi.anno.Limit)")
    public Object interceptor(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Limit limitAnnotation = method.getAnnotation(Limit.class);
        LimitType limitType = limitAnnotation.limitType();
        int limitPeriod = limitAnnotation.period();
        int limitCount = limitAnnotation.count();
        /**
         * 根据限流类型获取不同的key ,如果不传我们会以方法名作为key
         */
        String key;
        switch (limitType) {
            case IP:
                key = getIpAddress();
                break;
            case CUSTOMER:
                key = limitAnnotation.key();
                break;
            case METHOD_NAME:
                String methodName = method.getName();
                key = StringUtils.upperCase(methodName);
                break;
            default:
                throw new RuntimeException("limitInterceptor - 无效的枚举值");
        }
        /**
         * 获取注解标注的 key,这个是优先级最高的,会覆盖前面的 key 值
         */
        Object[] args = pjp.getArgs();
        Annotation[][] paramAnnoAry = method.getParameterAnnotations();
        for (Annotation[] item : paramAnnoAry) {
            int paramIndex = ArrayUtils.indexOf(paramAnnoAry, item);
            for (Annotation anno : item) {
                if (anno instanceof LimitKey) {
                    Object arg = args[paramIndex];
                    if (arg instanceof String && StringUtils.isNotBlank((String) arg)) {
                        key = (String) arg;
                        break;
                    }
                }
            }
        }
        if (StringUtils.isBlank(key)) {
            throw new RuntimeException("limitInterceptor - key值不能为空");
        }
        String prefix = limitAnnotation.prefix();
        String[] keyAry = StringUtils.isBlank(prefix) ? new String[]{"limit", key} : new String[]{"limit", prefix, key};
        ImmutableList<String> keys = ImmutableList.of(StringUtils.join(keyAry, "-"));
        try {
            String luaScript = buildLuaScript();
            RedisScript<Number> redisScript = new DefaultRedisScript<Number>(luaScript, Number.class);
            Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
            if (count != null && count.intValue() <= limitCount) {
                return pjp.proceed();
            } else {
                String classPath = method.getDeclaringClass().getName() + "." + method.getName();
                throw new RuntimeException("limitInterceptor - 限流被触发:"
                        + "class:" + classPath
                        + ", keys:" + keys
                        + ", limitcount:" + limitCount
                        + ", limitPeriod:" + limitPeriod + "s");
            }
        } catch (Throwable e) {
            if (e instanceof RuntimeException) {
                throw new RuntimeException(e.getLocalizedMessage());
            }
            throw new RuntimeException("limitInterceptor - 限流服务异常");
        }
    }
    /**
     * lua 脚本,为了保证执行 redis 命令的原子性
     */
    public String buildLuaScript() {
        StringBuilder lua = new StringBuilder();
        lua.append("local c");
        lua.append("\nc = redis.call(&#39;get&#39;,KEYS[1])");
        // 调用不超过最大值,则直接返回
        lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
        lua.append("\nreturn c;");
        lua.append("\nend");
        // 执行计算器自加
        lua.append("\nc = redis.call(&#39;incr&#39;,KEYS[1])");
        lua.append("\nif tonumber(c) == 1 then");
        // 从第一次调用开始限流,设置对应键值的过期
        lua.append("\nredis.call(&#39;expire&#39;,KEYS[1],ARGV[2])");
        lua.append("\nend");
        lua.append("\nreturn c;");
        return lua.toString();
    }
    public String getIpAddress() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}
Copier après la connexion

TestService

Exemple d'utilisation

    @Limit(period = 10, count = 10)
    public String delUserByUrlTest(@LimitKey String token, String thirdId, String url) throws IOException {
        return "success";
    }
Copier après la connexion

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:yisu.com
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal