上一篇记录了gateway网关的基础功能和配置,并且使用了默认的限流功能。
springcloud gateway网关-CSDN博客
这里简单记录一下gateway网关集成mybatisPlus实现动态限流。gateway网关默认的限流方式各项限流参数都是在配置文件中配置,不够灵活,虽然使用阿里的Sentinel组件可以实现从nacos注册中心、配置中心动态读取配置,但是还是有一定的局限性。
有些业务上需要限流功能可以在平台的页面上进行灵活配置,并且实时生效。
大致流程:数据库添加一个流控表,有需要限流的URL,最大限流限制数、时间范围等字段。通过页面维护这个表的数据。gateway中写一个全局过滤器中,收到请求后,用URL去数据库中查询、或者从缓存查询,得到需要限制的参数,再调用写好的限流方法实现限流。限流方法用Redis的Zset数据结构实现的滑动窗口算法,当然,也可以用其他的限流算法。
下面的配置是基于上一篇文章的代码来实现。
一、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: 8089spring:application:name: gatewaydatasource:url: jdbc:mysql://127.0.0.1:3306/test-db?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Drivertype: com.alibaba.druid.pool.DruidDataSourcedruid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)# 连接池的配置信息# 初始化大小,最小,最大initial-size: 5min-idle: 5maxActive: 20# 配置获取连接等待超时的时间maxWait: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒timeBetweenEvictionRunsMillis: 60000# 配置一个连接在池中最小生存的时间,单位是毫秒minEvictableIdleTimeMillis: 300000validationQuery: SELECT 1testWhileIdle: truetestOnBorrow: falsetestOnReturn: false# 打开PSCache,并且指定每个连接上PSCache的大小poolPreparedStatements: truemaxPoolPreparedStatementPerConnectionSize: 20# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙#filters: stat,wall,slf4j# 通过connectProperties属性来打开mergeSql功能;慢SQL记录connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000webStatFilter:enabled: true########## Redis ############redis:database: 0host: 127.0.0.1port: 6379password:########## gateway 相关配置 ############cloud:gateway:routes:- id: service-01uri: http://127.0.0.1:8080predicates:- Path=/svs1/**filters:- StripPrefix=1 # 去掉path前缀,1代表去掉第一个- name: RequestRateLimiterargs:redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充数redis-rate-limiter.burstCapacity: 1 #令牌容量key-resolver: "#{@apiKeyResolver}" # 限流策略,对应配置中的Bean- id: service-02uri: http://127.0.0.1:8080predicates:- Path=/svs2/**filters:- StripPrefix=1#mybatis plus 设置
mybatis-plus:mapper-locations: classpath:mapper/*.xmlglobal-config:# 关闭MP3.0自带的bannerbanner: falsedb-config:#主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)";id-type: AUTO# 默认数据库表下划线命名table-underline: trueconfiguration:# 这个配置会将执行的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:";@Autowiredprivate 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 {@Autowiredprivate RedisCache redisCache;@SneakyThrows@Overridepublic 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*/@Overridepublic 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("请稍后再试!");}}
}
这就完事!
读取数据库相关的简单业务代码就不做过多展示了,具体细节也是根据业务而定,这里只记录一下实现思路和核心流控代码。