这里简单记录一下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,功能齐全,自带控制台、仪表盘。