Redis + Lua 实现分布式应用限流

前言

今天讲的 redis+lua 解决分布式限流 任何框架都能用,只要能集成 redis就可以,不管是微服务 dubbo、springcloud,还是直接用 springboot或者 springMVC都通用的方法。

前面我们已经讲了三篇关于 网关做限流的解决方案了,可查看链接

  • https://blog.csdn.net/weixin_38003389/article/details/88992478
  • https://blog.csdn.net/weixin_38003389/article/details/88999062
  • https://blog.csdn.net/weixin_38003389/article/details/89000754

以上基于网关做限流操作,除了在 class 里面配置点东西,还需要在 yml 文件写配置,所以我这次使用 redis+lua 做限流,只需要在配置文件写你的 redis 配置就好了, 剩下的都交给 java 来处理。

东西好不好,大家往下看就清楚了,并且直接拿到你们项目里用就ok。

正文

 

介绍一下本次使用所有框架和中间件的版本

环境

框架

版本

Spring Boot

2.0.3.RELEASE

Spring Cloud

Finchley.RELEASE

redis

redis-4.0.11

JDK

1.8.x

 

前置准备工作

  1. 本机安装一个 redis ,端口按默认的,然后启动。
  2. 创建一个 eureka-service ,端口是 8888,然后启动。
  3. 父工程pom文件,滑动滚轮即可看到pom 的内容。

父pom


        ch3-4-eureka-client
        ch3-4-eureka-server
        redis-tool
    

    
        org.springframework.boot
        spring-boot-starter-parent
        2.0.3.RELEASE
         
    

    
        UTF-8
        UTF-8
        1.8
        Finchley.RELEASE
    

    
        
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
        
    

    
        
            org.springframework.boot
            spring-boot-starter-actuator
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
            org.springframework.boot
            spring-boot-starter-aop
        
    

 

核心代码示例

首先我们创建一个本次核心的工程,这个工程完全可以是你们项目里公共工程的其中一个文件夹,但在我的 Demo 中我这个重要的工程起名叫 redis-tool。

我们看这个工程所用到的依赖均在 父pom 中,需要有 aop 和redis 的依赖。

接下来看一下 我使用的 Lua 脚本,以下内容复制到 该项目的 resources 目录下,起名 limit.lua 即可。

local key = "rate.limit:" .. KEYS[1] --限流KEY
local limit = tonumber(ARGV[1])        --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
  return 0
else  --请求数+1,并设置2秒过期
  redis.call("INCRBY", key,"1")
   redis.call("expire", key,"2")
   return current + 1
end

解释 Lua 脚本含义:

  • 我们通过KEYS[1] 获取传入的key参数 
  • 通过ARGV[1]获取传入的limit参数 
  • redis.call方法,从缓存中get和key相关的值,如果为null那么就返回0 
  • 接着判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0 
  • 如果未超过,那么该key的缓存值+1,并设置过期时间为1秒钟以后,并返回缓存值+1
     

 

限流注解

 我们自定义一个注解,用来其他服务做限流使用的。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义限流注解
 *
 * @author yanlin
 * @version v1.3
 * @date 2019-04-05 7:58 PM
 * @since v8.0
 **/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    String key() default "limit";

    int time() default 5;

    int count() default 5;
}

 

配置类

@Component
public class Commons {

    @Bean
    public DefaultRedisScript redisluaScript() {
        DefaultRedisScript redisScript = new DefaultRedisScript<>();
//读取 lua 脚本
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
        redisScript.setResultType(Number.class);
        return redisScript;
    }
    @Bean
    public RedisTemplate limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

}

 

拦截器

/**
 * 拦截器
 * @author yanlin
 * @version v1.3
 * @date 2019-04-05 8:06 PM
 * @since v8.0
 **/
@Aspect
@Configuration
public class LimitAspect {
    private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);

    @Autowired
    private RedisTemplate limitRedisTemplate;

    @Autowired
    private DefaultRedisScript redisluaScript;

    @Around("execution(* cn.springcloud.book.controller ..*(..) )")
    public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Class targetClass = method.getDeclaringClass();
        RateLimit rateLimit = method.getAnnotation(RateLimit.class);

        if (rateLimit != null) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String ipAddress = getIpAddr(request);

            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append(ipAddress).append("-")
                    .append(targetClass.getName()).append("- ")
                    .append(method.getName()).append("-")
                    .append(rateLimit.key());

            List keys = Collections.singletonList(stringBuffer.toString());

            Number number = limitRedisTemplate.execute(redisluaScript, keys, rateLimit.count(), rateLimit.time());

            if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
                logger.info("限流时间段内访问第:{} 次", number.toString());
                return joinPoint.proceed();
            }

        } else {
            return joinPoint.proceed();
        }
//由于本文没有配置公共异常类,如果配置可替换
        throw new RuntimeException("已经到设置限流次数");
    }

    private 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按照','分割
            if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }

}

代码解释:

拦截器 拦截 @RateLimit 注解的方法,使用Redsi execute 方法执行我们的限流脚本,判断是否超过限流次数,

我们这里 execution 参数 在你们实际项目中需要变更,一半都会定位到你们 controller层。

 

创建 eureka-client 工程

该工程在项目中就是你们需要做限流的服务,我们创建 一个maven 项目 ,端口设定8081,然后下面是 我 redis 的配置

redis 配置

spring:
  # Redis数据库索引
  redis:
    database: 0
  # Redis服务器地址
    host: 127.0.0.1
  # Redis服务器连接端口
    port: 6379
  # Redis服务器连接密码(默认为空)
    password:
  # 连接池最大连接数(使用负值表示没有限制)
    jedis:
      pool:
        max-active: 8
  # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
  # 连接池中的最大空闲连接
        max-idle: 8
  # 连接池中的最小空闲连接
        min-idle: 0
  # 连接超时时间(毫秒)
    timeout: 10000

 

pom 文件


        cn.springcloud.book
        ch3-4
        0.0.1-SNAPSHOT
        ../pom.xml 
    

    
        UTF-8
        UTF-8
        1.8
    

    
        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-client
        
        
            redis-tool
            redis-tool
            0.0.1-SNAPSHOT
        

    

 

控制层

我们在 controller 层的方法上加上我们的自定义限流注解,可以按照我们的默认值,也可以自定义参数。

@RestController
public class LimitController {


    @RateLimit(key = "test", time = 10, count = 10)
    @GetMapping("/test/limit")
    public String testLimit() {
        return "Hello,ok";
    }

    @RateLimit()
    @GetMapping("/test/limit/a")
    public String testLimitA() {
        return "Hello,ok";
    }
}

 启动类

 有一个关键的地方,我们需要引用 redis-tool 中的包,但是利用@ComponentScan 注解也会排除本工程的包,所以,这里我们要写上本工程包 和redis-tool中的包,一会在下面看我工程结构就明白了。

@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(value = {"com.annotaion", "cn.springcloud", "com.config"})

public class Ch34EurekaClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(Ch34EurekaClientApplication.class, args);
    }
}

 

测试

我们最好把 redis-tool 项目 install 一下,然后启动 eureka-client 工程,访问 http://localhost:8081/test/limit/a  5秒中超过5次访问 页面和后端就会报错。

Redis + Lua 实现分布式应用限流_第1张图片

Redis + Lua 实现分布式应用限流_第2张图片

 

当然我们不能说流量被限制了给用户一个500吧,所以我们还需要对后端对报错进行一个统一拦截,springboot 统一异常处理我已经出过文章了, 请点击 https://blog.csdn.net/weixin_38003389/article/details/83149252 进行配置,这样就可以友好对输出到前端。

 

本文参考文献:https://blog.csdn.net/yanpenglei/article/details/81772530

作者叫磊哥,加我微信可以联系到本人哦

 

Redis + Lua 实现分布式应用限流_第3张图片

 

你可能感兴趣的:(java,redis,redis+lua限流,限流,redis限流,redis+lua)