【框架整合】Redis限流方案

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

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

2、准备

  • 引入相关依赖
xml 复制代码
<dependency>
<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
   <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.23</version>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.1.5</version>
</dependency>
<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>2.2</version>
</dependency>
<dependency>
  <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
  <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.25.1</version>
</dependency>
  • 添加redis配置信息
yaml 复制代码
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
java 复制代码
@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脚本
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
java 复制代码
@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. 增加注解类型
java 复制代码
public enum LimitType {
    /**
     * 默认策略全局限流
     */
    DEFAULT,
    /**
     * 根据请求者IP进行限流
     */
    IP
}
  1. 添加IPUtils
java 复制代码
@Slf4j
public class IpUtils {
    /**ip的长度值*/
    private static final int IP_LEN = 15;
    /** 使用代理时,多IP分隔符*/
    private static final String SPLIT_STR = ",";

    /**
     * 获取IP地址
     * <p>
     * 使用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. 核心处理类
java 复制代码
@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();
    }
}

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

相关推荐
敏姐的后花园2 小时前
模考倒计时网页版
java·服务器·前端
小满、2 小时前
MySQL :实用函数、约束、多表查询与事务隔离
数据库·mysql·事务·数据库函数·多表查询
百***35333 小时前
PostgreSQL_安装部署
数据库·postgresql
Dcs4 小时前
Java 中 UnaryOperator 接口与 Lambda 表达式的应用示例
java·后端
thekenofdis5 小时前
Lua脚本执行多个redis命令提示“CROSSSLOT Keys in request don‘t hash to the same slot“问题
redis·lua·哈希算法
rayylee5 小时前
生活抱怨与解决方案app
数据库·生活
bagadesu6 小时前
使用Docker构建Node.js应用的详细指南
java·后端
没有bug.的程序员6 小时前
Spring Cloud Gateway 性能优化与限流设计
java·spring boot·spring·nacos·性能优化·gateway·springcloud
Lucifer三思而后行6 小时前
使用 BR 备份 TiDB 到 AWS S3 存储
数据库·tidb·aws