>  기사  >  데이터 베이스  >  SpringBoot+Redis+Lua 분산 전류 제한 구현 방법

SpringBoot+Redis+Lua 분산 전류 제한 구현 방법

PHPz
PHPz앞으로
2023-05-28 08:55:051122검색

LUA 스크립트를 지원하는 Redis의 주요 이점

LUA 스크립트의 통합으로 Redis 데이터베이스에 대한 더 많은 사용 시나리오가 생성되고 더 많은 새로운 이점이 제공됩니다.

  • 효율성: 네트워크 오버헤드 및 지연 감소, 여러 Redis 서버 네트워크 요청됨 LUA 스크립트를 사용하여 한 번의 요청으로 작업을 완료할 수 있습니다

  • 데이터 신뢰성: Redis는 전체 스크립트를 전체적으로 실행하며 중간에 다른 명령이 삽입되지 않습니다.

  • 재사용성: LUA 스크립트가 실행된 후 Redis 서버에 영구적으로 저장되며 다른 클라이언트에서 직접 재사용할 수 있습니다.

  • 임베디드 가능성: JAVA, C# 및 기타 프로그래밍 언어에 내장될 수 있습니다. 다양한 운영 체제 지원 크로스 플랫폼 상호 작용

  • 간단하고 강력함: 작고 가벼움, 낮은 리소스 사용량, 절차적 및 객체 지향 프로그래밍 언어 지원

직장에서 언어 lua를 사용하는 것도 이번이 처음입니다. 기록하세요

Lua 파일 req_ratelimit.lua

local key = KEYS[1]   --限流KEY
local limitCount = tonumber(ARGV[1])       --限流大小
local limitTime = tonumber(ARGV[2])        --限流时间
local current = redis.call('get', key);
if current then
    if current + 1 > limitCount then --如果超出限流大小
        return 0
    else
        redis.call("INCRBY", key,"1")
        return current + 1
    end
else
    redis.call("set", key,"1")
    redis.call("expire", key,limitTime)
    return 1
end

SpringBoot+Redis+Lua 분산 전류 제한 구현 방법

사용자 정의 주석 RateLimiter

package com.shinedata.ann;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
 
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
 
    /**
     * 限流唯一标识
     * @return
     */
    String key() default "rate.limit:";
 
    /**
     * 限流时间
     * @return
     */
    int time() default 1;
 
    /**
     * 限流次数
     * @return
     */
    int count() default 100;
 
    /**
     *是否限制IP,默认 否
     * @return
     */
    boolean restrictionsIp() default false;
}

정의 측면 RateLimiterAspect

package com.shinedata.aop;
 
import com.shinedata.ann.RateLimiter;
import com.shinedata.config.redis.RedisUtils;
import com.shinedata.exception.RateLimiterException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
 
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
 
/**
 * @ClassName RateLimiterAspect
 * @Author yupanpan
 * @Date 2020/5/6 13:46
 */
@Aspect
@Component
public class RateLimiterAspect {
 
    private final Logger logger	= LoggerFactory.getLogger(this.getClass());
 
    private static ThreadLocal<String> ipThreadLocal=new ThreadLocal();
 
    private DefaultRedisScript<Number> redisScript;
 
    @PostConstruct
    public void init(){
        redisScript = new DefaultRedisScript<Number>();
        redisScript.setResultType(Number.class);
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/req_ratelimit.lua")));
    }
 
    @Around("@annotation(com.shinedata.ann.RateLimiter)")
    public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            Class<?> targetClass = method.getDeclaringClass();
            RateLimiter rateLimit = method.getAnnotation(RateLimiter.class);
 
            if (rateLimit != null) {
                HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                boolean restrictionsIp = rateLimit.restrictionsIp();
                if(restrictionsIp){
                    ipThreadLocal.set(getIpAddr(request));
                }
 
                StringBuffer stringBuffer = new StringBuffer();
                stringBuffer.append(rateLimit.key());
                if(StringUtils.isNotBlank(ipThreadLocal.get())){
                    stringBuffer.append(ipThreadLocal.get()).append("-");
                }
                stringBuffer.append("-").append(targetClass.getName()).append("- ").append(method.getName());
 
                List<String> keys = Collections.singletonList(stringBuffer.toString());
 
                Number number = RedisUtils.execute(redisScript, keys, rateLimit.count(), rateLimit.time());
 
                if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
                    logger.info("限流时间段内访问第:{} 次", number.toString());
                    return joinPoint.proceed();
                }else {
                    logger.error("已经到设置限流次数,当前次数:{}",number.toString());
                    throw new RateLimiterException("服务器繁忙,请稍后再试");
                }
            } else {
                return joinPoint.proceed();
            }
        }finally {
            ipThreadLocal.remove();
        }
    }
 
    public 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按照&#39;,&#39;分割
            if (ipAddress != null && ipAddress.length() > 15) {
                // "***.***.***.***".length()= 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }
}

Spring 데이터 redis는 Lua와 redis를 사용하여 상호 작용하는 DefaultRedisScript를 제공합니다. IP가 가변적이므로 여기서는 ThreadLocal을 사용합니다. 자신의 스레드 IP가 다른 스레드에 의해 수정되지 않도록 하세요. 메모리 누수를 방지하려면 마지막에 ThreadLocal을 정리해야 합니다.(메서드가 너무 많음) , 실행 메소드만 표시됨)

package com.shinedata.config.redis;
 
import org.checkerframework.checker.units.qual.K;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.stereotype.Component;
import org.springframework.util.CollectionUtils;
 
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
 
/**
 * @ClassName RedisUtils
 * @Author yupanpan
 * @Date 2019/11/20 13:38
 */
@Component
public class RedisUtils {
 
    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate<String, Object> redisTemplate;
 
    private static RedisUtils redisUtils;
    
    @PostConstruct
    public void init() {
        redisUtils = this;
        redisUtils.redisTemplate = this.redisTemplate;
    }
 
    public static Number execute(DefaultRedisScript<Number> script, List keys, Object... args) {
        return redisUtils.redisTemplate.execute(script, keys,args);
    }
}

직접 구성 RedisTemplate

package com.shinedata.config.redis;
 
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
 
/**
 * @ClassName RedisConfig
 * @Author yupanpan
 * @Date 2019/11/20 13:26
 */
@Configuration
public class RedisConfig extends RedisProperties{
 
    protected Logger log = LogManager.getLogger(RedisConfig.class);
 
    /**
     * JedisPoolConfig 连接池
     * @return
     */
    @Bean("jedisPoolConfig")
    public JedisPoolConfig jedisPoolConfig() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        // 最大空闲数
        jedisPoolConfig.setMaxIdle(500);
        jedisPoolConfig.setMinIdle(100);
        // 连接池的最大数据库连接数
        jedisPoolConfig.setMaxTotal(6000);
        // 最大建立连接等待时间
        jedisPoolConfig.setMaxWaitMillis(5000);
        // 逐出连接的最小空闲时间 默认1800000毫秒(30分钟)
        jedisPoolConfig.setMinEvictableIdleTimeMillis(100);
        // 每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3
//        jedisPoolConfig.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
        // 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
        jedisPoolConfig.setTimeBetweenEvictionRunsMillis(600);
        // 是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个
        jedisPoolConfig.setTestOnBorrow(true);
        // 在空闲时检查有效性, 默认false
        jedisPoolConfig.setTestWhileIdle(false);
        return jedisPoolConfig;
    }
 
    /**
     * JedisConnectionFactory
     * @param jedisPoolConfig
     */
    @Bean("jedisConnectionFactory")
    public JedisConnectionFactory jedisConnectionFactory(@Qualifier("jedisPoolConfig")JedisPoolConfig jedisPoolConfig) {
        JedisConnectionFactory JedisConnectionFactory = new JedisConnectionFactory(jedisPoolConfig);
        // 连接池
        JedisConnectionFactory.setPoolConfig(jedisPoolConfig);
        // IP地址
        JedisConnectionFactory.setHostName(redisHost);
        // 端口号
        JedisConnectionFactory.setPort(redisPort);
        // 如果Redis设置有密码
         JedisConnectionFactory.setPassword(redisPassword);
        // 客户端超时时间单位是毫秒
        JedisConnectionFactory.setTimeout(10000);
        return JedisConnectionFactory;
    }
 
    /**
     * 实例化 RedisTemplate 对象代替原有的RedisTemplate<String, String>
     * @return
     */
    @Bean("redisTemplate")
    public RedisTemplate<String, Object> functionDomainRedisTemplate(@Qualifier("jedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        initDomainRedisTemplate(redisTemplate, redisConnectionFactory);
        return redisTemplate;
    }
 
    /**
     * 设置数据存入 redis 的序列化方式
     * @param redisTemplate
     * @param factory
     */
    private void initDomainRedisTemplate(RedisTemplate<String, Object> redisTemplate, RedisConnectionFactory factory) {
        // 如果不配置Serializer,那么存储的时候缺省使用String,比如如果用User类型存储,那么会提示错误User can&#39;t cast
        // to String!
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 开启事务/true必须手动释放连接,false会自动释放连接 如果调用方有用@Transactional做事务控制,可以开启事务,Spring会处理连接问题
        redisTemplate.setEnableTransactionSupport(false);
        redisTemplate.setConnectionFactory(factory);
    }
}

전역 컨트롤러 예외 처리 GlobalExceptionHandler

package com.shinedata.exception;
 
import com.fasterxml.jackson.databind.JsonMappingException;
import com.shinedata.util.ResultData;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
 
@RestControllerAdvice
public class GlobalExceptionHandler {
 
    private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    @ExceptionHandler(value = RateLimiterException.class)
    @ResponseStatus(HttpStatus.OK)
    public ResultData runtimeExceptionHandler(RateLimiterException e) {
        logger.error("系统错误:", e);
        return ResultData.getResultError(StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "处理失败");
    }
 
    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.OK)
    public ResultData runtimeExceptionHandler(RuntimeException e) {
        Throwable cause = e.getCause();
        logger.error("系统错误:", e);
        logger.error(e.getMessage());
        if (cause instanceof JsonMappingException) {
            return ResultData.getResultError("参数错误");
        }
        return ResultData.getResultError(StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "处理失败");
    }
 
}

사용이 매우 간단하며 주석 하나만 추가됩니다

SpringBoot+Redis+Lua 분산 전류 제한 구현 방법추가: Lua는

local key = KEYS[1]
local limitCount = tonumber(ARGV[1])
local limitTime = tonumber(ARGV[2])
local current = redis.call(&#39;get&#39;, key);
if current then
    redis.call("INCRBY", key,"1")
    return current + 1
else
    redis.call("set", key,"1")
    redis.call("expire", key,limitTime)
    return 1
end
에 최적화되어 있습니다.

위 내용은 SpringBoot+Redis+Lua 분산 전류 제한 구현 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제