【框架整合】Redis限流方案

1、Redis实现限流方案的核心原理:

redis实现限流的核心原理在于redis 的key 过期时间,当我们设置一个key到redis中时,会将key设置上过期时间,这里的实现是采用lua脚本来实现原子性的。

2、准备

  • 引入相关依赖
<dependency>
<groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
   <groupId>cn.hutoolgroupId>
    <artifactId>hutool-allartifactId>
    <version>5.8.23version>
dependency>
<dependency>
   <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
    <version>3.1.5version>
dependency>
<dependency>
    <groupId>org.yamlgroupId>
    <artifactId>snakeyamlartifactId>
    <version>2.2version>
dependency>
<dependency>
  <groupId>org.apache.commonsgroupId>
    <artifactId>commons-pool2artifactId>
dependency>
<dependency>
  <groupId>com.google.protobufgroupId>
    <artifactId>protobuf-javaartifactId>
    <version>3.25.1version>
dependency>
  • 添加redis配置信息
server:
  port: 6650

nosql:
  redis:
    host: XXX.XXX.XXX.XXX
    port: 6379
    password:
    database: 0

spring:
  cache:
    type: redis
  redis:
    host: ${nosql.redis.host}
    port: ${nosql.redis.port}
    password: ${nosql.redis.password}
    lettuce:
      pool:
        enabled: true
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: 1000
  • 配置redis Conf
@Configuration
public class RedisConfig {


    /**
     * 序列化
     * jackson2JsonRedisSerializer
     *
     * @param redisConnectionFactory 复述,连接工厂
     * @return {@link RedisTemplate}<{@link Object}, {@link Object}>
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        template.setKeySerializer(jackson2JsonRedisSerializer);
        template.setHashKeySerializer(jackson2JsonRedisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
    /**
     * 加载lua脚本
     * @return {@link DefaultRedisScript}<{@link Long}>
     */
    @Bean
    public DefaultRedisScript<Long> limitScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("luaFile/rateLimit.lua")));
        redisScript.setResultType(Long.class);
        return redisScript;
    }

}

3、限流实现

  1. 编写核心lua脚本
local key = KEYS[1]
-- 取出key对应的统计,判断统计是否比限制大,如果比限制大,直接返回当前值
local count = tonumber(ARGV[1])
local time = tonumber(ARGV[2])
local current = redis.call('get', key)
if current and tonumber(current) > count then
    return tonumber(current)
end
--如果不比限制大,进行++,重新设置时间
current = redis.call('incr', key)
if tonumber(current) == 1 then
    redis.call('expire', key, time)
end
return tonumber(current)
  1. 编写注解 limiter
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
    /**
     * 限流key
     */
    String key() default "rate_limit:";

    /**
     * 限流时间,单位秒
     */
    int time() default 60;

    /**
     * 限流次数
     */
    int count() default 100;

    /**
     * 限流类型
     */
    LimitType limitType() default LimitType.DEFAULT;
}
  1. 增加注解类型
public enum LimitType {
    /**
     * 默认策略全局限流
     */
    DEFAULT,
    /**
     * 根据请求者IP进行限流
     */
    IP
}
  1. 添加IPUtils
@Slf4j
public class IpUtils {
    /**ip的长度值*/
    private static final int IP_LEN = 15;
    /** 使用代理时,多IP分隔符*/
    private static final String SPLIT_STR = ",";

    /**
     * 获取IP地址
     * 

* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 */ public static String getIpAddr(HttpServletRequest request) { String ip = null; try { ip = request.getHeader("x-forwarded-for"); if (StrUtil.isBlank(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (StrUtil.isBlank(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (StrUtil.isBlank(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (StrUtil.isBlank(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (StrUtil.isBlank(ip)) { ip = request.getRemoteAddr(); } } catch (Exception e) { log.error("IPUtils ERROR ", e); } //使用代理,则获取第一个IP地址 if (!StrUtil.isBlank(ip) && ip.length() > IP_LEN) { if (ip.indexOf(SPLIT_STR) > 0) { ip = ip.substring(0, ip.indexOf(SPLIT_STR)); } } return ip; } }

  1. 核心处理类
@Aspect
@Component
@Slf4j
public class RateLimiterAspect {
    @Resource
    private RedisTemplate<Object, Object> redisTemplate;

    @Resource
    private RedisScript<Long> limitScript;

    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        String key = rateLimiter.key();
        int time = rateLimiter.time();
        int count = rateLimiter.count();

        String combineKey = getCombineKey(rateLimiter, point);
        List<Object> keys = Collections.singletonList(combineKey);
        try {
            Long number = redisTemplate.execute(limitScript, keys, count, time);
            if (number == null || number.intValue() > count) {
                throw new ServiceException("访问过于频繁,请稍候再试");
            }
            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("服务器限流异常,请稍候再试");
        }
    }

    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        StringBuilder stringBuilder = new StringBuilder(rateLimiter.key());
        if (rateLimiter.limitType() == LimitType.IP) {
            stringBuilder.append(IpUtils.getIpAddr(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest())).append("-");
        }
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        stringBuilder.append(targetClass.getName()).append("-").append(method.getName());
        return stringBuilder.toString();
    }
}

到此,我们就可以利用注解,对请求方法进行限流了

你可能感兴趣的:(框架整合,redis,数据库,缓存,java,intellij-idea)