前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis + Lua 实现分布式应用限流

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

作者头像
胖虎
发布2019-06-26 17:13:03
1.1K1
发布2019-06-26 17:13:03
举报
文章被收录于专栏:晏霖晏霖

前言

今天讲的 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

代码语言:javascript
复制
<modules>
        <module>ch3-4-eureka-client</module>
        <module>ch3-4-eureka-server</module>
        <module>redis-tool</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    </dependencies>

核心代码示例

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

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

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

代码语言:javascript
复制
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 脚本含义:

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

限流注解

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

代码语言:javascript
复制
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;
}
代码语言:javascript
复制

配置类

代码语言:javascript
复制
@Component
public class Commons {

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

}

拦截器

代码语言:javascript
复制
/**
 * 拦截器
 * @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<String, Serializable> limitRedisTemplate;

    @Autowired
    private DefaultRedisScript<Number> 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<String> 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 配置

代码语言:javascript
复制
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 文件

代码语言:javascript
复制
<parent>
        <groupId>cn.springcloud.book</groupId>
        <artifactId>ch3-4</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>redis-tool</groupId>
            <artifactId>redis-tool</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

    </dependencies>

控制层

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

代码语言:javascript
复制
@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中的包,一会在下面看我工程结构就明白了。

代码语言:javascript
复制
@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次访问 页面和后端就会报错。

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

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

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 晏霖 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 正文
    • 环境
      • 前置准备工作
        • 核心代码示例
          • 创建 eureka-client 工程
          相关产品与服务
          云数据库 Redis
          腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档