ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Aop限流实现解决方案

2022-05-17 01:00:44  阅读:139  来源: 互联网

标签:ip 解决方案 Aop redis springframework 限流 org import


01、限流

在业务场景中,为了限制某些业务的并发,造成接口的压力,需要增加限流功能。

02、限流的成熟解决方案

  • guava (漏斗算法 + 令牌算法) (单机限流)
  • redis + lua + ip 限流(比较推荐)(分布式限流)
  • nginx 限流 (源头限流)

03、 限流的目的

  • 保护服务的资源泄露
  • 解决服务器的高可压,减少服务器并发

04、安装redis服务

(1)安装redis
wget http://download.redis.io/releases/redis-6.0.6.tar.gz
tar xzf redis-6.0.6.tar.gz
cd redis-6.0.6
make
(2)修改redis.conf
daemonize yes
# bind 127.0.0.1
protected-mode no
requirepass 123456
(3)如果你之前启动过redis服务器,请麻烦一定要先检查,把服务杀掉,在启动
ps -ef | grep redis
kill redispid
(4)然后重启服务,一定指定配置文件启动
./src/redis-server ./redis.conf

05、创建springboot项目整合redis

image

(1)导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.qbb.limit</groupId>
    <artifactId>redis-lua-limit</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>redis-lua-limit</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>31.0.1-jre</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

(2)修改配置文件

server:
  port:9001
spring:
  redis:
    host: 192.168.137.72
    port: 6379
    database: 0
    lettuce:
      pool:
        max-active: 20
        max-wait: -1
        max-idle: 5
        min-idle: 0
  application:
    name: redis-lua-limit

(3)创建一个Redis配置类

说明一下:为什么要创建一个redis配置类,直接用SpringBoot自动装配的RedisTemplate不行么?
主要原因是:springboot本身在RedisAutoConfiguration里面已经初始化好了RedisTemplate。但是这个RedisTemplate序列化key的时候是以Object的类型进行序列化,所以看到 "\xac\xed\x00\x05t\x00\x14age11111111111111111" 字符串不友好。所以就写一个配置类进行覆盖了。

package com.qbb.limit.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
 * @version 1.0
 * @date 2022-05-16  23:34
 * @Description:
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 1: 开始创建一个redistemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 2:开始redis连接工厂跪安了
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 创建一个json的序列化方式
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // 设置key用string序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置value用jackjson进行处理
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash也要进行修改
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        // 默认调用
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}

(4)定义限流lua脚本(这个可以使用我下面提供的,也可以在网上直接百度,如果感兴趣也可以自己研究研究)

在resources目录下的lua文件夹下,新建一个iplimite.lua文件,文件内容如下:

-- 为某个接口的请求IP设置计数器,比如:127.0.0.1请求接口
-- KEYS[1] = 127.0.0.1 也就是用户的IP
-- ARGV[1] = 过期时间 30m
-- ARGV[2] = 限制的次数
local limitCount = redis.call('incr',KEYS[1]);
if limitCount == 1 then
    redis.call("expire",KEYS[1],ARGV[2])
end
-- 如果次数还没有过期,并且还在规定的次数内,说明还在请求同一接口
if limitCount > tonumber(ARGV[1]) then
    return false
end

return true

(5)在config包中创建一个LuaConfig的Lua限流脚本配置类

lua配置类主要是去加载lua文件的内容,放到内存中。方便redis去读取和控制。
image

package com.qbb.limit.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

/**
 * @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
 * @version 1.0
 * @date 2022-05-16  23:45
 * @Description:
 */
@Configuration
public class LuaConfig {
    /**
     * 将lua脚本的内容加载出来放入到DefaultRedisScript
     *
     * @return
     */
    @Bean
    public DefaultRedisScript<Boolean> ipLimitLua() {
        DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/iplimite.lua")));
        defaultRedisScript.setResultType(Boolean.class);
        return defaultRedisScript;
    }
}

(6)自定义一个限流注解(为什么要用注解,两个字:方便)

image

package com.qbb.limit.aop;

import java.lang.annotation.*;

/**
 * @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
 * @version 1.0
 * @date 2022-05-16  23:57
 * @Description:
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiter {
    // 每timeout限制请求的个数
    int limit() default 10;

    // 时间,单位默认是秒
    int timeout() default 1;
}

(7)创建一个获取用户访问IP的工具类(网上百度的)

package com.qbb.limit.utils;

import javax.servlet.http.HttpServletRequest;

/**
 * @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
 * @version 1.0
 * @date 2022-05-17  0:01
 * @Description:
 */
public class RequestUtils {

    public static String getIpAddr(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }
        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("X-Forwarded-For");
        }
        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.getHeader("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }
}

(8)定义核心限流AOP切面类

package com.qbb.limit.core;

import com.google.common.collect.Lists;
import com.qbb.limit.aop.AccessLimiter;
import com.qbb.limit.utils.RequestUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;

/**
 * @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
 * @version 1.0
 * @date 2022-05-17  0:06
 * @Description:
 */
@Component
@Aspect
@Slf4j
public class LimiterAspect {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private DefaultRedisScript<Boolean> ipLimiterLuaScript;
    @Autowired
    private DefaultRedisScript<Boolean> ipLimitLua;

    // 1: 切入点
    @Pointcut("@annotation(com.qbb.limit.aop.AccessLimiter)")
    public void limiterPointcut() {
    }

    @Before("limiterPointcut()")
    public void limiter(JoinPoint joinPoint) {
        log.info("限流进来了.......");
        // 1:获取方法的签名作为key
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String classname = methodSignature.getMethod().getDeclaringClass().getName();
        String packageName = methodSignature.getMethod().getDeclaringClass().getPackage().getName();
        log.info("classname:{},packageName:{}", classname, packageName);
        // 4: 读取方法的注解信息获取限流参数
        AccessLimiter annotation = method.getAnnotation(AccessLimiter.class);
        // 5:获取注解方法名
        String methodNameKey = method.getName();
        // 6:获取服务请求的对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        HttpServletResponse response = requestAttributes.getResponse();
        String userIp = RequestUtils.getIpAddr(request);
        log.info("用户IP是:.......{}", userIp);
        // 7:通过方法反射获取注解的参数
        Integer limit = annotation.limit();
        Integer timeout = annotation.timeout();
        String redisKey = method + ":" + userIp;
        // 8: 请求lua脚本
        Boolean acquired = stringRedisTemplate.execute(ipLimitLua, Lists.newArrayList(redisKey), limit.toString(), timeout.toString());
        // 如果超过限流限制
        if (!acquired) {
            // 抛出异常,然后让全局异常去处理
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html;charset=UTF-8");

            try (PrintWriter writer = response.getWriter();) {
                // 解决报错:getWriter() has already been called for this response] with root cause
                writer.print("<h1>客官你慢点,请稍后在试一试!!!</h1>");
                writer.flush();
            } catch (Exception ex) {
                throw new RuntimeException("客官你慢点,请稍后在试一试!!!");
            }
        }
    }
}

(9)编写测试代码

package com.qbb.limit.controller;

import com.qbb.limit.aop.AccessLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author QiuQiu&LL (个人博客:https://www.cnblogs.com/qbbit)
 * @version 1.0
 * @date 2022-05-17  0:16
 * @Description:
 */
@RestController
public class HelloController {
    @GetMapping("/hello")
    @AccessLimiter(timeout = 1, limit = 3) // 1秒钟超过3次限流
    public String index() {
        // 分布锁
        return "success";
    }

    @GetMapping("/hello2")
    public String index2() {
        return "success";
    }
}

访问一次没问题
image

快速刷新试试
image

标签:ip,解决方案,Aop,redis,springframework,限流,org,import
来源: https://www.cnblogs.com/qbbit/p/16279283.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有