springcloud gateway网关动态配置限流

上一篇记录了gateway网关的基础功能和配置,并且使用了默认的限流功能。

springcloud gateway网关-CSDN博客

这里简单记录一下gateway网关集成mybatisPlus实现动态限流。gateway网关默认的限流方式各项限流参数都是在配置文件中配置,不够灵活,虽然使用阿里的Sentinel组件可以实现从nacos注册中心、配置中心动态读取配置,但是还是有一定的局限性。

有些业务上需要限流功能可以在平台的页面上进行灵活配置,并且实时生效。

大致流程:数据库添加一个流控表,有需要限流的URL,最大限流限制数、时间范围等字段。通过页面维护这个表的数据。gateway中写一个全局过滤器中,收到请求后,用URL去数据库中查询、或者从缓存查询,得到需要限制的参数,再调用写好的限流方法实现限流。限流方法用Redis的Zset数据结构实现的滑动窗口算法,当然,也可以用其他的限流算法。

目录

一、pom文件中添加依赖

二、配置文件

三、相关代码

四、总结


下面的配置是基于上一篇文章的代码来实现。

一、pom文件中添加依赖

mybatisPlus相关依赖

        <!-- 数据库驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.27</version>
        </dependency>

        <!-- druid数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.16</version>
        </dependency>

        <!-- mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

        <!-- druid数据库连接池 需要用到该依赖 ,否则启动报错-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>

二、配置文件

server:
  port: 8089

spring:
  application:
    name: gateway
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test-db?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
      # 连接池的配置信息
      # 初始化大小,最小,最大
      initial-size: 5
      min-idle: 5
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      #filters: stat,wall,slf4j
      # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
      webStatFilter:
        enabled: true




########## Redis ############
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    password:

  ########## gateway 相关配置 ############
  cloud:
    gateway:
      routes:
        - id: service-01
          uri: http://127.0.0.1:8080
          predicates:
            - Path=/svs1/**
          filters:
            - StripPrefix=1 # 去掉path前缀,1代表去掉第一个
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充数
                redis-rate-limiter.burstCapacity: 1 #令牌容量
                key-resolver: "#{@apiKeyResolver}" # 限流策略,对应配置中的Bean

        - id: service-02
          uri: http://127.0.0.1:8080
          predicates:
            - Path=/svs2/**
          filters:
            - StripPrefix=1


#mybatis plus 设置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  global-config:
    # 关闭MP3.0自带的banner
    banner: false
    db-config:
      #主键类型  0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)";
      id-type: AUTO
      # 默认数据库表下划线命名
      table-underline: true
  configuration:
    # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 返回类型为Map,显示null对应的字段
    call-setters-on-nulls: true

三、相关代码

1)redis工具类

package com.zhh.gateway.common.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @Description: Redis缓存
 * @Author: zhaoheng
 * @CreateTime: 2024-03-13  21:10
 */
@Component
public class RedisCache {

    public static final String SYS_PREFIX = "gateway:";

    @Autowired
    private RedisTemplate redisTemplate;

    public String getZSetKey(String key) {
        return SYS_PREFIX + "zset:" + key;
    }

    /**
     * zSet数据结构添加数据
     * @param key		唯一标识
     * @param value		值
     * @param score		分值,用于排序
     * @param expireTime	过期时间,单位:秒
     * @param <T>
     */
    public <T> void zSetAdd(String key, T value, double score, long expireTime) {
        key = getZSetKey(key);
        ZSetOperations zSetOps = redisTemplate.opsForZSet();
        zSetOps.add(key, value, score);
        zSetOps.getOperations().expire(key, expireTime, TimeUnit.SECONDS);
    }
    /**
     * 删除指定范围内的数据
     * @param key		唯一标识
     * @param min		最小值
     * @param max		最大值
     * @return
     */
    public Long zSetRemoveRangeByScore(String key, double min, double max) {
        return redisTemplate.opsForZSet().removeRangeByScore(getZSetKey(key),min,max);
    }
    /**
     * 统计数据总量
     * @param key		唯一标识
     * @return
     */
    public Long zSetCountAll(String key) {
        return redisTemplate.opsForZSet().zCard(getZSetKey(key));
    }
}

2)限流过滤器实现

核心就是Redis实现的滑动窗口的限流算法

package com.zhh.gateway.filter;

import com.zhh.gateway.common.util.RedisCache;
import com.zhh.gateway.pojo.ApiLimiterPO;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @Description: 全局过滤器 限流过滤器
 * @Author: zhaoheng
 * @CreateTime: 2024
 */
@Slf4j
@Component
public class ApiLimiterFilter implements GlobalFilter, Ordered {

    @Autowired
    private RedisCache redisCache;

    @SneakyThrows
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getPath().value();
        log.info("request url:{}", url);
        // TODO 从请求头或cookie中获取签名,解析得到用户唯一标识
        String userId = "zs";
        this.apiLimiterByUser(url, userId);
        return chain.filter(exchange);
    }

    /**
     * IApiLimiterService
     * 过滤器执行顺序,值越小越靠前
     *
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }

    /**
     * 根据用户唯一标识限流
     *
     * @param reqUrl 请求
     * @param userId 用户唯一标识
     * @throws Exception
     */
    public void apiLimiterByUser(String reqUrl, String userId) throws Exception {
        // TODO 根据URL从数据库中查询限流相关配置
        //ApiLimiterPO apiLimiterPO = iApiLimiterService.getByUrl(reqUrl);
        // 模拟从数据库中查询到的数据
        ApiLimiterPO apiLimiterPO = ApiLimiterPO.builder()
                .apiUrl("/api/v1.0/user/query")
                // 限流:1秒钟最多2个请求
                .rangeTime(1).limitMax(2)
                .build();
        // 没有查询到数据,说明该接口没有配置限流
        if (null == apiLimiterPO) {
            log.info("无需限流,url:{}", reqUrl);
            return;
        }
        log.info("apiLimiterPO:{}", apiLimiterPO.toString());
        // 根据用户id限流
        String key = "xl:" + userId;
        // 时间窗口大小 限流:【rangeTime】秒钟最多【limitMax】个请求
        int rangeTime = apiLimiterPO.getRangeTime();
        // 流量大小
        int limitMax = apiLimiterPO.getLimitMax();
        // 当前时间
        long now = System.currentTimeMillis();
        // Redis实现滑动窗口算法  删除【rangeTime】秒之前的数据
        redisCache.zSetRemoveRangeByScore(key, 0, now - (rangeTime * 1000));
        // 添加请求数据到Redis,设置过期时间
        redisCache.zSetAdd(key, now, now, 60 * 60);
        // 统计总数据量
        Long sum = redisCache.zSetCountAll(key);
        if (sum > limitMax) {
            // TODO 一般都是自定义异常,然后全局异常处理器再统一返回错误信息给调用端
            throw new Exception("请稍后再试!");
        }
    }
}

这就完事!

读取数据库相关的简单业务代码就不做过多展示了,具体细节也是根据业务而定,这里只记录一下实现思路和核心流控代码。

四、总结

如果不使用gateway网关,还有两种比较好的限流方式

1)在需要限流的项目中使用aop+自定义注解,或者拦截器加自定义注解,在需要限流的接口上添加自定义注解,注解属性中填上接口的唯一标识,该唯一标识和数据库限流表中的数据一一对应,可通过页面维护,aop或者拦截器中得到注解上的唯一标识,用唯一标识查询数据库得到限流参数,进行限流。

2)使用阿里巴巴开源的限流框架Sentinel,功能齐全,自带控制台、仪表盘。

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值