【框架整合】Redis限流方案

news/2024/7/8 2:35:45 标签: redis, 数据库, 缓存, java, intellij-idea

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

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

2、准备

  • 引入相关依赖
<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>
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
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脚本
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();
    }
}

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


http://www.niftyadmin.cn/n/5196634.html

相关文章

优思学院|什么是精益生产管理?从一个生活上的故事出发来说明。

你关掉电脑&#xff0c;离开办公室。 一个小时后&#xff0c;你进入家门和孩子们在一起。 你和家人一起吃晚饭。 你的老板打电话来查看你的项目进展。 你哄孩子入睡并给他们读个故事。 作为一个负责任的父母&#xff0c;你想要与孩子们的互动时间增加并提高生活的质量&…

[Kettle] 生成记录

在数据统计中&#xff0c;往往要生成固定行数和列数的记录&#xff0c;用于存放统计总数 需求&#xff1a;为方便记录1~12月份商品的销售总额&#xff0c;需要通过生成记录&#xff0c;生成一个月销售总额的数据表&#xff0c;包括商品名称和销售总额两个字段&#xff0c;记录…

(论文阅读51-57)图像描述3 53

51.文献阅读笔记&#xff08;KNN&#xff09; 简介 题目 Exploring Nearest Neighbor Approaches for Image Captioning 作者 Jacob Devlin, Saurabh Gupta, Ross Girshick, Margaret Mitchell, C. Lawrence Zitnick, arXiv:1505.04467 原文链接 http://arxiv.org/pdf/1…

不想花大价钱?这10款替代Axure的平替软件更划算!

Axure是许多产品经理和设计师进入快速原型设计的首选工具&#xff0c;但Axure的使用成本相对较高&#xff0c;学习曲线陡峭&#xff0c;许多设计师正在寻找可以取代Axure的原型设计工具&#xff0c;虽然现在有很多可选的设计工具&#xff0c;但质量不均匀&#xff0c;可以取代A…

数据结构与算法编程题1

从顺序表中删除具有最小的元素&#xff08;假设唯一&#xff09;并由函数返回被删除元素的值&#xff0c; 空出的位置由最后一个元素填补&#xff0c;若顺序表为空&#xff0c;则显示出错信息并退出运行。 #include <iostream> using namespace std;typedef int ElemTyp…

纵行科技亮相2023汽车物流行业年会,与菜鸟共推ZETag资产管理方案

近日&#xff0c;由中物联汽车物流分会主办的“汽车物流行业年会”在十堰召开。纵行科技受邀亮相&#xff0c;并与菜鸟共推ZETag资产管理方案&#xff0c;助力汽车物流数字化发展。 当前&#xff0c;我国物流业处于恢复性增长和结构性调整的关键期&#xff0c;国务院印发的《…

Deque继承ArrayDeque和继承LinkedList区别在哪里

在Java中&#xff0c;ArrayDeque和LinkedList都是Deque接口的实现类&#xff0c;但它们的内部实现和性能特性有一些不同。 ArrayDeque&#xff1a; 内部实现&#xff1a;ArrayDeque使用动态数组&#xff08;resizable array&#xff09;来实现&#xff0c;它允许在两端高效地进…

[C#]ListEtcDemo

代码1: using System; using System.Collections.Generic; using System.Linq;namespace ListEtcDemo2 {public class Formteacher{public int ID { set; get; }public string FormteahcerName { set; get; }public string Remark { set; get; }}public class Student{public …