这里只贴出核心依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
<version>3.0.3version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redis-reactiveartifactId>
<version>2.5.3version>
dependency>
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.stereotype.Component;
/**
* @author whitebrocade
* @version 1.0
* @description: 重写RequestRateLimiterGatewayFilterFactory中apply方法
*
*/
@Slf4j
@Component
public class MyRequestRateLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory {
/**
* 间隔符号
*/
private static final String interval_mark = "_";
public MyRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {
super(defaultRateLimiter, defaultKeyResolver);
}
@Override
public GatewayFilter apply(RequestRateLimiterGatewayFilterFactory.Config config) {
// 防止重新相同路由id下的规则在map出现覆盖的问题, 改写key的组成规则
// routeId -> routeId + 限流策略的hash取值 -> 保证了同一路由下, 相同id下的限流规则不会被覆盖
String routeId = config.getRouteId() + interval_mark + config.getKeyResolver().hashCode();
log.info("MultiRequestRateLimiterGatewayFilterFactory::routeId={}", routeId);
config.setRouteId(routeId);
return super.apply(config);
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* @author whitebrocade
* @version 1.0
* @description: 重写RequestRateLimiterGatewayFilterFactory中apply方法
*
*/
@Slf4j
@Component
public class MyRequestRateLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory {
/**
* 从请求头中获取的user_id
*/
private static final String USER_ID = "user_id";
/**
* 间隔符号
*/
private static final String INTERVAL_MARK = "_";
/**
* 返回的提示信息
*/
private static final String MSG = "该接口请求频繁, 请稍后重试";
private final RateLimiter defaultRateLimiter;
private final KeyResolver defaultKeyResolver;
public MyRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {
super(defaultRateLimiter, defaultKeyResolver);
this.defaultRateLimiter = defaultRateLimiter;
this.defaultKeyResolver = defaultKeyResolver;
}
/**
* 重写apply自定义限流异常返回值以及解决同路由id下多个限流规则的覆盖问题
* @param config Config
* @return GatewayFilter
*/
@Override
public GatewayFilter apply(Config config) {
KeyResolver resolver = this.getOrDefault(config.getKeyResolver(), defaultKeyResolver);
RateLimiter<Object> limiter = this.getOrDefault(config.getRateLimiter(), defaultRateLimiter);
return (exchange, chain) -> resolver.resolve(exchange).flatMap(key -> {
String routeId = config.getRouteId();
if (routeId == null) {
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
// 将routeId修改成routeId + 限流策略的hash取值, 保证了同路由id的多个限流规则不会被覆盖
routeId = route.getId() + INTERVAL_MARK + resolver.hashCode();
} else {
// 将routeId修改成routeId + 限流策略的hash取值, 保证了同路由id的多个限流规则不会被覆盖
routeId = config.getRouteId() + INTERVAL_MARK + resolver.hashCode();
}
String finalRouteId = routeId;
return limiter.isAllowed(routeId, key).flatMap(response -> {
for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
}
if (response.isAllowed()) {
return chain.filter(exchange);
}
String userId = exchange.getRequest().getHeaders().get(USER_ID).get(0);
// todo 触发限流后, 这里可以尝试编码收集一些数据进行统计, 根据统计结果去做一些逻辑处理
log.warn("用户: {}, 触发限流的路由id: {}", userId, finalRouteId);
// 自定义返回值
ServerHttpResponse httpResponse = exchange.getResponse();
// 修改code为429: 请求频繁
httpResponse.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
if (!httpResponse.getHeaders().containsKey("Content-Type")) {
// 返回类型设置为JSON
httpResponse.getHeaders().add("Content-Type", "application/json");
}
// 注意事项: 此处无法触发全局异常处理,所以手动封装返回
DataBuffer buffer = httpResponse.bufferFactory().wrap((
"{\n"
+ " \"code\": \"" + HttpStatus.TOO_MANY_REQUESTS.value() + "\","
+ " \"msg\": \"" + MSG + "\""
+ "}").getBytes(StandardCharsets.UTF_8));
return httpResponse.writeWith(Mono.just(buffer));
});
});
}
private <T> T getOrDefault(T configValue, T defaultValue) {
return (configValue != null) ? configValue : defaultValue;
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.event.FilterArgsEvent;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.cloud.gateway.support.ConfigurationService;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.List;
import java.util.Map;
/**
* @author whitebrocade
* @version 1.0
* @description: 自定义的限流器
*/
@Slf4j
public class MyRequestRateLimiter extends RedisRateLimiter {
/**
* 配置文件中自定义流策略的的key
*/
private static final String KEY_RESOLVER_KEY = "key-resolver";
/**
* 间隔符号
*/
private static final String INTERVAL_MARK = "_";
public MyRequestRateLimiter(ReactiveStringRedisTemplate redisTemplate, RedisScript<List<Long>> script, ConfigurationService configurationService) {
super(redisTemplate, script, configurationService);
}
/**
* 重写的是处理FilterArgsEvent事件的回调方法, 改写路由id, 保证了同一个路由下配置的多个限流规则不会出现覆盖的现象
* @param event FilterArgsEvent
*/
@Override
public void onApplicationEvent(FilterArgsEvent event) {
Map<String, Object> args = event.getArgs();
if (args.containsKey(KEY_RESOLVER_KEY)) {
String routeId = event.getRouteId() + INTERVAL_MARK + args.get(KEY_RESOLVER_KEY).hashCode();
super.onApplicationEvent(new FilterArgsEvent(event.getSource(), routeId, args));
}
super.onApplicationEvent(event);
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.support.ConfigurationService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 限流规则配置类
*/
@Slf4j
@Configuration
public class KeyResolverConfiguration {
/**
* 从请求头中获取的值
*/
private static final String USER_ID = "user_id";
/**
* userId限流解析策略
* @return KeyResolver
*/
@Bean
@Primary
KeyResolver userIdResolver(){
return exchange -> {
String userId = exchange.getRequest().getHeaders().get(USER_ID).get(0);
String path = exchange.getRequest().getPath().toString();
String ipAddress = exchange.getRequest().getRemoteAddress().getHostName();
log.info("userIdResolver-网关:用户id:{}, 访问路径:{}, ip地址:{}", userId, path, ipAddress);
return Mono.just(userId);
};
}
/**
* 路径限流解析策略
* @return KeyResolver
*/
@Bean
KeyResolver pathResolver(){
return exchange -> {
String userId = exchange.getRequest().getHeaders().get(USER_ID).get(0);
String path = exchange.getRequest().getPath().toString();
String ipAddress = exchange.getRequest().getRemoteAddress().getHostName();
log.info("pathResolver-网关:用户id:{}, 访问路径:{}, ip地址:{}", userId, path, ipAddress);
return Mono.just(path);
};
}
/**
* ip限流策略
* @return KeyResolver
*/
@Bean
KeyResolver ipAddressResolver() {
return exchange -> {
String userId = exchange.getRequest().getHeaders().get(USER_ID).get(0);
String path = exchange.getRequest().getPath().toString();
String ipAddress = exchange.getRequest().getRemoteAddress().getHostName();
log.info("ipAddressResolver-网关:用户id:{}, 访问路径:{}, ip地址:{}", userId, path, ipAddress);
return Mono.just(ipAddress);
};
}
/*
自己定义的reactiveStringRedisTemplateFlowLimit
*/
@Qualifier("reactiveStringRedisTemplateFlowLimit")
ReactiveStringRedisTemplate redisTemplate;
/*
限流的Lua脚本
*/
@Qualifier(MyRequestRateLimiter.REDIS_SCRIPT_NAME)
RedisScript<List<Long>> redisScript;
@Bean
@Primary
public MyRequestRateLimiter myRedisRateLimiter(ConfigurationService configService) {
return new MyRequestRateLimiter(redisTemplate, redisScript, configService);
}
}
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.yhb.common.core.utils.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis配置
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* 网关redis限流使用的的ReactiveStringRedisTemplate(响应式的), 专门用于网关限流, 没有做对象的序列化
*/
@Bean
@SuppressWarnings("rawtypes")
public ReactiveStringRedisTemplate reactiveStringRedisTemplateFlowLimit(
@Value("${spring.redis-flow-limit.database}") int database,
@Value("${spring.redis.lettuce.pool.max-active}") int maxActive,
@Value("${spring.redis.lettuce.pool.max-wait}") int maxWait,
@Value("${spring.redis.lettuce.pool.max-idle}") int maxIdle,
@Value("${spring.redis.lettuce.pool.min-idle}") int minIdle,
@Value("${spring.redis.host}") String hostName,
@Value("${spring.redis.port}") int port,
@Value("${spring.redis.password}") String password)
{
/* ========= 基本配置 ========= */
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
configuration.setHostName(hostName);
configuration.setPort(port);
configuration.setDatabase(database);
if (! StringUtils.isBlank(password)) {
RedisPassword redisPassword = RedisPassword.of(password);
configuration.setPassword(redisPassword);
}
/* ========= 连接池通用配置 ========= */
GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
genericObjectPoolConfig.setMaxTotal(maxActive);
genericObjectPoolConfig.setMinIdle(minIdle);
genericObjectPoolConfig.setMaxIdle(maxIdle);
genericObjectPoolConfig.setMaxWaitMillis(maxWait);
/* ========= lettuce pool ========= */
LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder();
builder.poolConfig(genericObjectPoolConfig);
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration, builder.build());
connectionFactory.afterPropertiesSet();
/* ========= 创建 template ========= */
return new ReactiveStringRedisTemplate(connectionFactory);
}
}
spring:
redis:
host: 127.0.0.1
port: 6379
# 有密码就配置, 没有就为空即可
password:
lettuce:
pool:
max-idle: 30
max-active: 8
max-wait: 10000
min-idle: 10
# 网关限流, 限流相关操作的在其他索引库进行
redis-flow-limit:
# 使用一号索引库
database: 1
spring:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
# 商户接口
- id: whitebrocade-merchant
uri: lb://whitebrocade-merchant
predicates:
- Path=/merchant/**
filters:
- StripPrefix=1
# --- -----------网关限流相关配置-start--------------
# 默认在服务名后添加-limit-flow区分普通路由和限流路由
- id: whitebrocade-merchant-flow-limit
uri: lb://whitebrocade-merchant
predicates:
- Path=/merchant/pay/myPay
# 值越小越先匹配,优先级高于默认的0,保证流控的路由优先匹配
# 如果不设置的话, 匹配的时候就是从上往下匹配, 也就是优先走了路由whitebrocade-merchant, 为了避免手动调整路由的顺序的繁琐, 所以手动指定order
order: -1
# 基于令牌桶实现,持有令牌才能访问, 没有令牌就限流
filters:
- StripPrefix=1
# 自定义的限流过滤器
- name: MyRequestRateLimiter
args:
# 使用SpEL表达式从Spring容器中获取限流策略的bean对象, SqEL表达式不清楚去百度一下
key-resolver: "#{@userIdResolver}"
# 令牌桶每秒填充平均速率, 允许用户每秒处理多少个请求, 不支持设置小数(即设置了也是无效的)
redis-rate-limiter.replenishRate: 1
# 令牌桶的容量,允许在1s内完成的最大请求数, 通常和replenishRate<=replenishRate
redis-rate-limiter.burstCapacity: 1
# 这是第二条, 可自行开启
# - name: MyRequestRateLimiter
# args:
# # 使用SpEL表达式从Spring容器中获取限流策略的bean对象, SqEL表达式不清楚去百度一下
# key-resolver: "#{@ipAddressResolver}"
# # 令牌桶每秒填充平均速率, 允许用户每秒处理多少个请求, 不支持设置小数(即设置了也是无效的)
# redis-rate-limiter.replenishRate: 1
# # 令牌桶的容量,允许在1s内完成的最大请求数, 通常和replenishRate<=replenishRate
# redis-rate-limiter.burstCapacity: 5
# --------------网关限流相关配置-end--------------
到此集成完毕, 测试接口会发现已经限流成功了, 这里就不演示了
问题现象: 阿里云redis集群无法限流, 提示一下异常Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException:
ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array, and KEYS should not be in expression
翻译过来大致的意思就是Redis集群中有错误lua脚本,脚本使用的所有keys都应该使用KEYS数组传递,并且键不应在表达式中
看一下gateway网关中使用的lua脚本, 位于gateway依赖包下的META-INF/scripts/request_rate_limiter.lua
gateway原Lua脚本如下
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
if ttl > 0 then
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
end
-- return { allowed_num, new_tokens, capacity, filled_tokens, requested, new_tokens }
return { allowed_num, new_tokens }
下边这版是删除了部分注释代码, 以及添加了部分注释的
-- token的key
local tokens_key = KEYS[1]
-- 时间戳的key
local timestamp_key = KEYS[2]
-- 往令牌桶里面放令牌的速率,一秒多少个
local rate = tonumber(ARGV[1])
-- 令牌桶最大容量
local capacity = tonumber(ARGV[2])
-- 当前的时间戳
local now = tonumber(ARGV[3])
-- 请求消耗令牌的数量
local requested = tonumber(ARGV[4])
-- 计算放满令牌桶的所需时长
local fill_time = capacity/rate
-- redis过期时间 这里为什么是放满令牌桶的两倍
-- 因为这个时间不能太长,加入太长10s,你第一秒把令牌拿完,后面9s,就会出现突刺现象
local ttl = math.floor(fill_time*2)
-- 获取令牌桶的数量,如果为空,将令牌桶容量赋值给当前token
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
-- 获取最后的更新时间戳,如果为空,设置为0
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
-- 计算出时间间隔
local delta = math.max(0, now-last_refreshed)
-- 该往令牌桶放令牌的数量
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
-- 看剩余的令牌是否能够获取到
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
-- 零代表false, 即是限流
local allowed_num = 0
-- 如果允许获取得到,计算出剩余的令牌数量,并标记可以获取
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
-- 存到redis
if ttl > 0 then
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
end
-- lua可以返回多个字段,java获取时用List获取
return { allowed_num, new_tokens }
核心问题就在
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
这里将KEYS[1]和 KEYS[2]赋值给了变量, 然后传递给了后续的代码
然而, 为了保证脚本里面的所有操作都在相同slot进行,云数据库Redis集群版本会对Lua脚本做如下限制
所有key都应该由KEYS数组来传递redis.call/pcall中调用的redis命令,key的位置必须是KEYS array(不能使用Lua变量替换KEYS),否则直接返回错误信息:
ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS arrayrn所有key必须在一个slot上,否则返回错误信息
: ERR eval/evalsha command keys must be in same slotrn调用必须要带有key,否则直接返回错误信息
: ERR for redis cluster, eval/evalsha number of keys can’t be negative or zerorn核心: 然而gateway自带原Lua脚本违背了第一条, 使用Lua变量替换了KEYS
将脚本的进行替换, 为了方便观察我非相关的注释删除, 同时对改动的地方进行标注(每一个改动的地方都使用数字标注)
local tokens_key = KEYS[1]
和local timestamp_key = KEYS[2]
(这里为了方便观察我就注释掉了)tokens_key
的地方替换成KEYS[1]
timestamp_key
的地方替换成KEYS[2]
-- local tokens_key = KEYS[1] -- 1. 注释掉这行代码
-- local timestamp_key = KEYS[2] -- 2. 注释掉这行代码
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
-- local last_tokens = tonumber(redis.call("get", tokens_key)) -- 3.1 将这行代码的tokens_key修改成KEYS[1]
local last_tokens = tonumber(redis.call("get", KEYS[1])) -- 3.2 修改后的代码
if last_tokens == nil then
last_tokens = capacity
end
-- local last_refreshed = tonumber(redis.call("get", timestamp_key)) -- 4.1 将这行代码的timestamp_key修改成KEYS[2]
local last_refreshed = tonumber(redis.call("get", KEYS[2])) -- 4.2 修改后的代码
if last_refreshed == nil then
last_refreshed = 0
end
local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
if ttl > 0 then
-- redis.call("setex", tokens_key, ttl, new_tokens) -- 5.1 将这行代码的tokens_key修改成KEYS[1]
redis.call("setex", KEYS[1], ttl, new_tokens) -- 5.2 修改后的代码
-- redis.call("setex", timestamp_key, ttl, now) -- 6.1 将这行代码的timestamp_key修改成KEYS[2]
redis.call("setex", timestamp_key, ttl, now) -- 6.2 修改后的代码
end
return { allowed_num, new_tokens }
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.event.FilterArgsEvent;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator;
import org.springframework.cloud.gateway.support.ConfigurationService;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author whitebrocade
* @version 1.0
* @description: 自定义的限流器
*/
@Slf4j
@ConfigurationProperties("spring.cloud.gateway.redis-rate-limiter")
public class MyRequestRateLimiter extends RedisRateLimiter {
/**
* 配置文件中自定义流策略的的key
*/
private static final String KEY_RESOLVER_KEY = "key-resolver";
/**
* 间隔符号
*/
private static final String INTERVAL_MARK = "_";
public MyRequestRateLimiter(ReactiveStringRedisTemplate redisTemplate, RedisScript<List<Long>> script, ConfigurationService configurationService) {
super(redisTemplate, script, configurationService);
}
@Override
public void onApplicationEvent(FilterArgsEvent event) {
Map<String, Object> args = event.getArgs();
if (args.containsKey(KEY_RESOLVER_KEY)) {
String routeId = event.getRouteId() + INTERVAL_MARK + args.get(KEY_RESOLVER_KEY).hashCode();
super.onApplicationEvent(new FilterArgsEvent(event.getSource(), routeId, args));
}
super.onApplicationEvent(event);
}
// -------------下边是重写isAllowed()需要的逻辑
@Autowired
@Qualifier("reactiveStringRedisTemplateFlowLimit")
ReactiveStringRedisTemplate redisTemplate;
private Config defaultConfig;
private AtomicBoolean initialized = new AtomicBoolean(false);
/**
* 限流lua脚本
*/
private final String luaScriptStr = "local rate = tonumber(ARGV[1])\n" +
"local capacity = tonumber(ARGV[2])\n" +
"local now = tonumber(ARGV[3])\n" +
"local requested = tonumber(ARGV[4])\n" +
"\n" +
"local fill_time = capacity/rate\n" +
"local ttl = math.floor(fill_time*2)\n" +
"\n" +
"local last_tokens = tonumber(redis.call(\"get\", KEYS[1]))\n" +
"if last_tokens == nil then\n" +
" last_tokens = capacity\n" +
"end\n" +
"\n" +
"local last_refreshed = tonumber(redis.call(\"get\", KEYS[2]))\n" +
"if last_refreshed == nil then\n" +
" last_refreshed = 0\n" +
"end\n" +
"\n" +
"local delta = math.max(0, now-last_refreshed)\n" +
"\n" +
"local filled_tokens = math.min(capacity, last_tokens+(delta*rate))\n" +
"local allowed = filled_tokens >= requested\n" +
"local new_tokens = filled_tokens\n" +
"local allowed_num = 0\n" +
"\n" +
"if allowed then\n" +
" new_tokens = filled_tokens - requested\n" +
" allowed_num = 1\n" +
"end\n" +
"\n" +
"if ttl > 0 then\n" +
" redis.call(\"setex\", KEYS[1], ttl, new_tokens)\n" +
" redis.call(\"setex\", KEYS[2], ttl, now)\n" +
"end\n" +
"\n" +
"return { allowed_num, new_tokens }\n";
@Override
public Mono<Response> isAllowed(String routeId, String id) {
Config routeConfig = loadConfiguration(routeId);
int replenishRate = routeConfig.getReplenishRate();
int burstCapacity = routeConfig.getBurstCapacity();
int requestedTokens = routeConfig.getRequestedTokens();
try {
// keys参数
List<String> keys = getKeys(id);
// args脚本参数
List<String> scriptArgs = Arrays.asList(
replenishRate + "",
burstCapacity + "",
Instant.now().getEpochSecond() + "",
requestedTokens + "");
// 执行lua脚本
DefaultRedisScript<List> luaScript = new DefaultRedisScript<>(luaScriptStr, List.class);
Flux<List> flux = this.redisTemplate.execute(luaScript, keys, scriptArgs);
// 根据执行结果记录异常或将结果返回
return flux.onErrorResume(throwable -> {
log.error("无法调用rate的限流lua脚本: {}", JSONObject.toJSONString(flux), throwable);
// 将List 转换成 ArrayList
ArrayList<Long> arr = new ArrayList<>();
CollUtil.addAll(arr, Arrays.asList(1L, - 1L));
return Flux.just(arr);
}).reduce(new ArrayList<Long>(), (longs, l) -> {
longs.addAll(l);
return longs;
}).map(results -> { // 将响应结果返回
// 0-限流 1-通过
boolean allowed = results.get(0) == 1L;
Long tokensLeft = results.get(1);
Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft));
log.error("限流返回结果响应体response: " + response);
return response;
});
} catch (Exception e) {
log.error("Redis限流异常", e);
}
// 如果出现了异常, 那么就直接放行, 同时将剩余令牌数设置成-1
return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
}
static List<String> getKeys(String id) {
String prefix = "request_rate_limiter.{" + id;
String tokenKey = prefix + "}.tokens";
String timestampKey = prefix + "}.timestamp";
return Arrays.asList(tokenKey, timestampKey);
}
Config loadConfiguration(String routeId) {
Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);
if (routeConfig == null) {
routeConfig = getConfig().get(RouteDefinitionRouteLocator.DEFAULT_FILTERS);
}
if (routeConfig == null) {
throw new IllegalArgumentException("No Configuration found for route " + routeId + " or defaultFilters");
}
return routeConfig;
}
}
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
<version>3.0.3version>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
<version>2021.1version>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
<version>2021.1version>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
<version>2021.1version>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
<version>1.8.0version>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-spring-cloud-gateway-adapterartifactId>
<version>1.8.0version>
dependency>
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
/**
* 网关启动程序
*/
@Sl4j
@SpringBootApplication})
public class GatewayApplication {
public static void main(String[] args) {
// 启动参数设置type, 否则sentinel控制面板中是不会有API管理页面的
System.setProperty("csp.sentinel.app.type", "1");
SpringApplication.run(GatewayApplication.class, args);
log.info("\n >>>>>>>>>>>>>>>>>>>>>>> 网关服务启动成功 <<<<<<<<<<<<<<<<<<<<<<<< \n");
}
}
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.yhb.common.core.utils.ServletUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;
/**
* 自定义限流异常处理
*
*/
public class SentinelFallbackHandler implements WebExceptionHandler
{
private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange)
{
// 返回的状态码为429
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), HttpStatus.TOO_MANY_REQUESTS,"请求超过最大数,请稍后再试", HttpStatus.TOO_MANY_REQUESTS.value());
}
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex)
{
if (exchange.getResponse().isCommitted())
{
return Mono.error(ex);
}
if (!BlockException.isBlockException(ex))
{
return Mono.error(ex);
}
return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange));
}
private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable)
{
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
}
}
spring:
cloud:
sentinel:
scg:
fallback:
mode: response
# 响应码
response-status: 429
# 限流响应内容
response-body: '{"code":429,"message":"触发限流了"}'
# 响应类型
content-type: "application/json"
博主修改sentinel的账号密码以及端口, 大家根据自己的参数修改即可
spring:
cloud:
sentinel:
# 支持链路限流
# web-context-unify: false
# 关闭官方默认收敛所有context
# filter:
# enabled: true
# 取消控制台懒加载
eager: true
# 限流触发的提示信息
scg:
fallback:
mode: response
# 响应码
response-status: 429
# 限流响应内容
response-body: '{"code":429,"message":"触发限流了"}'
# 响应类型
content-type: "application/json"
transport:
# todo 控制台地址 目前是启用本地的, 默认端口是8848
dashboard: 127.0.0.1:8848
# todo 跟sentinel控制台交流的端口,随意指定一个未使用的端口即可,默认是8719
port: 8719
# sentinel规则持久化配置
datasource:
# 网关api分组
gw-api-group:
# nacos相关配置
nacos:
# 配置中心地址
server-addr: 127.0.0.1:8848
# 命名空间
namespace: whitebrocade
# 配置文件名
dataId: sentinel-gw-api-group
# 文件类型
data-type: json
# 规则类型: api分组
rule-type: gw-api-group
# 网关限流配置
gw-flow:
# nacos的下述参数不在赘述
nacos:
server-addr: 127.0.0.1:8848
namespace: whitebrocade
dataId: sentinel-gw-flow
data-type: json
rule-type: gw-flow
sentinel-gw-api-group.yaml
sentienl规则持久化, 里面的存储的规则都是JSON格式的
注意! 写入nacos的时候一定要把注释移除!!
[
{
// API名称
"apiName": "sentinel-gw-api-group",
"predicateItems": [
{
// 匹配策略
"matchStrategy": 0,
// 参数值
"pattern": "/whitebrocade/wallet/myWallet"
}
]
}
]
sentinel-gw-flow.yaml
[
{
// API类型: API分组
"resourceMode": 1,
// api名
"resource": "sentinel-gw-api-group",
// 流控效果: 默认(直接拒绝)
"controlBehavior": 0,
// 阈值类型: QPS模式
"grade": 1,
// 限流阈值
"count": 1,
// 滑动窗口时间
"intervalSec": 600,
// 流控模式: 直接
"strategy": 0,
// 突发请求允许流量
"burst": 0
}
]
还有部分参数, 由于目前没有用到不, 所以不选出来解释
需要修改sentinel的源码, 并且这里采用的是push模式, 将sentinel规则持久化到nacos中
将sentinel-datasource-nacos的test范围注释掉
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
引入下述依赖, 方便改造
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.24version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.11version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.8.25version>
dependency>
<dependency>
<groupId>com.alibaba.nacosgroupId>
<artifactId>nacos-clientartifactId>
<version>2.2.1version>
dependency>
修改成yaml格式也可以, 这里就不修改了
# nacos数据源配置
# nacos所在服务器地址
sentinel.nacos.serverAddr=127.0.0.1
# 账号
sentinel.nacos.username=nacos
# 密码
sentinel.nacos.password=nacos
# 命名空间
sentinel.nacos.namespace=whitebrocade
可以直接复制我的
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.*;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigFactory;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.Properties;
/**
* @author whitebrocade
* @version 1.0
* @description: nacos配置类
* 1. nacos配置文件读取, 将ConfigService注册成Bean对象
* 2. 配置相关流控规则的转换器(对象 -> JSON, JSON -> 对象), 具体有下列规则
* - 普通流控规则
* - 授权规则
* - 降级规则
* - 热点规则
* - 系统规则
* - 网关API分组管理规则
* - 网关流控规则
*/
@Slf4j
@Configuration
public class NacosConfig {
// -------------------- 配置文件读取 --------------------
/**
* nacos所在的服务器
*/
@Value("${sentinel.nacos.serverAddr}")
private String serverAddr;
/**
* nacos用户名
*/
@Value("${sentinel.nacos.username}")
private String username;
/**
* nacos密码
*/
@Value("${sentinel.nacos.password}")
private String password;
/**
* nacos命名空间
*/
@Value("${sentinel.nacos.namespace}")
private String namespace;
/**
* 读取nacos配置, 并将nacos配置服务注册为bean对象
*/
@Bean
public ConfigService nacosConfigService() throws Exception {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
properties.put(PropertyKeyConst.NAMESPACE, namespace);
properties.put(PropertyKeyConst.USERNAME, username);
properties.put(PropertyKeyConst.PASSWORD, password);
return ConfigFactory.createConfigService(properties);
}
// -------------------- 相关流控规则 --------------------
// ==================== 普通流控规则 ====================
/**
* 普通流控规则: 将List转成JSON字符串, 用于传输到nacos
*/
@Bean
public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
return JSON::toJSONString;
}
/**
* 普通流控规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
*/
@Bean
public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
return s -> JSON.parseArray(s, FlowRuleEntity.class);
}
// ==================== 授权规则 ====================
/**
* 授权规则: 将List转成JSON字符串, 用于传输到nacos
*/
@Bean
public Converter<List<AuthorityRuleEntity>, String> authorRuleEntityEncoder() {
return JSON::toJSONString;
}
/**
* 授权规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
*/
@Bean
public Converter<String, List<AuthorityRuleEntity>> authorRuleEntityDecoder() {
return s -> JSON.parseArray(s, AuthorityRuleEntity.class);
}
// ==================== 降级规则 ====================
/**
* 降级规则: 将List转成JSON字符串, 用于传输到nacos
*/
@Bean
public Converter<List<DegradeRuleEntity>, String> degradeRuleEntityEncoder() {
return JSON::toJSONString;
}
/**
* 降级规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
*/
@Bean
public Converter<String, List<DegradeRuleEntity>> degradeRuleEntityDecoder() {
return s -> JSON.parseArray(s, DegradeRuleEntity.class);
}
// ==================== 热点规则 ====================
/**
* 热点规则: 将List转成JSON字符串, 用于传输到nacos
*/
@Bean
public Converter<List<ParamFlowRuleEntity>, String> paramRuleEntityEncoder() {
return JSON::toJSONString;
}
/**
* 热点规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
*/
@Bean
public Converter<String, List<ParamFlowRuleEntity>> paramRuleEntityDecoder() {
return s -> JSON.parseArray(s, ParamFlowRuleEntity.class);
}
// ==================== 系统规则 ====================
/**
* 系统规则: 将List转成JSON字符串, 用于传输到nacos
*/
@Bean
public Converter<List<SystemRuleEntity>, String> systemRuleEntityEncoder() {
return JSON::toJSONString;
}
/**
* 系统规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
*/
@Bean
public Converter<String, List<SystemRuleEntity>> systemRuleEntityDecoder() {
return s -> JSON.parseArray(s, SystemRuleEntity.class);
}
// ==================== 网关API分组管理规则 ====================
/**
* 网关API分组管理规则: 将List转成JSON字符串, 用于传输到nacos
*/
@Bean
public Converter<List<ApiDefinitionEntity>, String> apiDefinitionEntityEncoder() {
return JSON::toJSONString;
}
/**
* 网关API分组管理规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
*/
@Bean
public Converter<String, List<ApiDefinitionEntity>> apiDefinitionEntityDecoder() {
return s -> JSON.parseArray(s, ApiDefinitionEntity.class);
}
// ==================== 网关流控规则 ====================
/**
* 网关流控规则: 将List转成JSON字符串, 用于传输到nacos
*/
@Bean
public Converter<List<GatewayFlowRuleEntity>, String> gatewayFlowRuleEntityEncoder() {
return entityList -> {
List<GatewayFlowRule> ruleList = entityList.stream()
.map(GatewayFlowRuleEntity::toGatewayFlowRule)
.collect(Collectors.toList());
String jsonStr = JSONObject.toJSONString(ruleList);
log.info("转换后的JSON字符串:{}", jsonStr);
return jsonStr;
};
}
/**
* 网关流控规则: 将nacos中的JSON字符串转成List, 后续sentinel-dashboard中进行可视化展示
*/
@Bean
public Converter<String, List<GatewayFlowRuleEntity>> gatewayFlowRuleEntityDecoder() {
return s -> JSON.parseArray(s, GatewayFlowRuleEntity.class);
}
}
package com.alibaba.csp.sentinel.dashboard.rule.nacos;
/**
* @author whitebrocade
* @version 1.0
* @description: nacos配置类工具, 主要定义一些流控规则文件的后缀名, 便于区分
*/
public class NacosConfigUtil {
/**
* nacos中sentinel流控使用的分组
*/
public static final String GROUP_ID = "SENTINEL_GROUP";
// -------------------- 流控规则的后缀 --------------------
/**
* 普通流控规则
*/
public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";
/**
* 权限规则
*/
public static final String AUTHORITY_DATA_ID_POSTFIX = "-authority-rules";
/**
* 降级规则
*/
public static final String DEGRADE_DATA_ID_POSTFIX = "-degrade-rules";
/**
* 热点规则
*/
public static final String PARAM_FLOW_DATA_ID_POSTFIX = "-param-flow-rules";
/**
* 系统规则
*/
public static final String SYSTEM_DATA_ID_POSTFIX = "-system-rules";
/**
* 网关API分组管理规则
*/
public static final String GATEWAY_API_GROUP_DATA_ID_POSTFIX = "-gw-api-group-rules";
/**
* 网关流控规则
*/
public static final String GATEWAY_FLOW_DATA_ID_POSTFIX = "-gw-flow-rules";
}
在src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos如下结构(目前使用网关限流进行说明, 如果需要其他的, 请自行拓展)
package com.alibaba.csp.sentinel.dashboard.rule.limit.gateway.api;
import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @author whitebrocade
* @version 1.0
* @description: 网关API分组规则的生产者, 拉取Nacos中存储的网关API分组规则配置信息, 到时候用于sentinel-dashboard的规则可视化
*/
@Component("gatewayApiNacosProvider")
public class GatewayApiNacosProvider implements DynamicRuleProvider<List<ApiDefinitionEntity>> {
/*
DynamicRuleProvider, 这里的T是要获取的流控实体类型, 可以填写下边的任意一个
1. 普通限流规则实体 -> FlowRuleEntity
2. 授权规则实体 -> AuthorityRuleEntity
3. 降级规则实体 -> DegradeRuleEntity
4. 热点规则实体 -> ParamFlowRuleEntity
5. 网关API分组管理规则 -> ApiDefinitionEntity
6. 网关流控规则 -> GatewayFlowRuleEntity
*/
/**
* 注入configService, 用于从nacos中读取相应的流控规则
*/
@Autowired
private ConfigService configService;
/**
* 注入转换器, 将nacos中相应的流控JSON字符串转换成与之对应的流控实体类对象
*/
@Autowired
private Converter<String, List<ApiDefinitionEntity>> converter;
@Override
public List<ApiDefinitionEntity> getRules(String appName) throws Exception {
/*
getConfig(String dataId, String group, long timeoutMs)
作用: 从nacos读取配置
方法参数解析:
- dataId: 这个规则在nacos中的dataId, 这里我们采用 服务名 + 指定后缀作为dataId, 比如我的服务名是"my-gateway", 而这里后缀我们定义的是"-gw-api-group-rules"
- group: 该配置文件的分组, 我们使用的是"SENTINEL_GROUP"
- timeoutMs: 超时毫秒数
*/
String rules = configService.getConfig(appName + NacosConfigUtil.GATEWAY_API_GROUP_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID,
3000);
if (StrUtil.isEmpty(rules)) {
return new ArrayList<>();
}
return converter.convert(rules);
}
}
package com.alibaba.csp.sentinel.dashboard.rule.limit.gateway.api;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.ConfigType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author whitebrocade
* @version 1.0
* @description: 网关API分组管理规则的发布者, 将sentinel-dashboard的配置的网关API分组管理规则推送到nacos中持久化存储
*/
@Component("gatewayApiNacosPublisher")
public class GatewayApiNacosPublisher implements DynamicRulePublisher<List<ApiDefinitionEntity>> { // DynamicRulePublisher> 这个T和之前的含义差不多, 这里就是你要推送的流控类型
/**
* 注入configService, 用于将流控规则推送到nacos中
*/
@Autowired
private ConfigService configService;
/**
* 注入转换器, 用于将流控规则转换为JSON字符串
*/
@Autowired
private Converter<List<ApiDefinitionEntity>, String> converter;
@Override
public void publish(String app, List<ApiDefinitionEntity> rules) throws Exception {
AssertUtil.notEmpty(app, "app的name不能为空");
// 如果规则为空就直接返回
if (CollUtil.isEmpty(rules)) {
return;
}
/*
configService.publishConfig(String dataId, String group, String content, String type)
作用: 将流控规则转换为JSON字符串, 并推送到nacos中
方法参数解析:
- dataId: nacos的dataId
- group: 该配置文件所在的分组
- content: 流控实体对象转换后的JSON字符串内容
- type: 该配置文件内容所属的类型, 一般为JSON
*/
configService.publishConfig(
app + NacosConfigUtil.GATEWAY_API_GROUP_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID,
converter.convert(rules),
ConfigType.JSON.getType());
}
}
注意了, 这里没有继承DynamicRulePublisher!而是直接自定义getRules方法
package com.alibaba.csp.sentinel.dashboard.rule.limit.gateway.flow;
import cn.hutool.core.util.StrUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @author whitebrocade
* @version 1.0
* @description: 网关限流规则的发布者, 将sentinel-dashboard的配置的网关限流规则推送到nacos中持久化存储
*/
@Slf4j
@Component("gatewayFlowRulesNacosProvider")
public class GatewayFlowRulesNacosProvider {
@Autowired
private ConfigService configService;
/**
* 获取网关流控规则
* @param app application服务名
* @param ip ip地址
* @param port 端口
* @return 转换号的List
* @throws Exception
*/
public List<GatewayFlowRuleEntity> getRules(String app, String ip, Integer port) throws Exception {
String jsonStr = configService.getConfig(
app + NacosConfigUtil.GATEWAY_FLOW_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID,
3000);
if (StrUtil.isEmpty(jsonStr)) {
return new ArrayList<>();
}
// 将获取到的JSON字符串转换成GatewayFlowRule列表
List<GatewayFlowRule> ruleList = JSON.parseArray(jsonStr, GatewayFlowRule.class);
// 将GatewayFlowRule列表转换成GatewayFlowRuleEntity列表
List<GatewayFlowRuleEntity> entityList = ruleList.stream()
.map(rule -> GatewayFlowRuleEntity.fromGatewayFlowRule(app, ip, port, rule))
.collect(Collectors.toList());
log.info("JSON字符串:{}, " +
"JSON->List:{}, " +
"List->List:{}," ,
jsonStr, JSONObject.toJSONString(ruleList), JSONObject.toJSONString(entityList));
return entityList;
}
}
注意了, 这里没有继承DynamicRulePublisher!而是直接自定义getRules方法
package com.alibaba.csp.sentinel.dashboard.rule.limit.gateway.flow;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.ConfigType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author whitebrocade
* @version 1.0
* @description: 网关限流规则的发布者, 将sentinel-dashboard的配置的网关限流规则推送到nacos中持久化存储
*/
@Slf4j
@Component("gatewayFlowRulesNacosPublisher")
public class GatewayFlowRulesNacosPublisher implements DynamicRulePublisher<List<ApiDefinitionEntity>> {
@Autowired
private ConfigService configService;
@Autowired
private Converter<List<ApiDefinitionEntity>, String> converter;
@Override
public void publish(String app, List<ApiDefinitionEntity> rules) throws Exception {
AssertUtil.notEmpty(app, "app的name不能为空");
if (CollUtil.isEmpty(rules)) {
return;
}
log.info("推送的限流规则:{}", JSONObject.toJSONString(rules));
configService.publishConfig(
app + NacosConfigUtil.GATEWAY_FLOW_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID,
converter.convert(rules),
ConfigType.JSON.getType());
}
}
其他的流控规则持久化写法大差不差, 唯一需要注意的点就是规则之间的转换这里由于篇幅限制, 不再逐个列举说明
GatewayApiController类所在路径:src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java
需要修改的接口概览
URL | 方法名 | 请求方式 | 接口说明 |
---|---|---|---|
/gateway/api/list.json | queryApis | GET | 获取所有的API分组管理规则列表 |
/gateway/api/new.json | addApi | POST | 新增API分组 |
/gateway/api/save.json | updateApi | POST | 修改当前存在的API分组信息 |
/gateway/api/delete.json | deleteApi | POST | 删除一条API分组信息 |
改动点概览:
@RestController
@RequestMapping(value = "/gateway/api")
public class GatewayApiController {
private final Logger logger = LoggerFactory.getLogger(GatewayApiController.class);
@Autowired
private InMemApiDefinitionStore repository;
// ---------------- 改动 start ----------------
/*
1. SentinelApiClient类主要负责与 Sentinel 客户端通信,会发送HTTP调用客户端的API接口进行数据交互。其中主要是通过定义的一些方法将网关规则从内存中进行存取操作,具体方法可以查看相关代码。由于当前需要对网关规则进行从nacos存取操作,所以这里将其进行注释掉
*/
// @Autowired
// private SentinelApiClient sentinelApiClient;
// 2. 注入自己的实现的GatewayApiNacosProvider和GatewayApiNacosPublisher
@Autowired
@Qualifier("gatewayApiNacosProvider")
private DynamicRuleProvider<List<ApiDefinitionEntity>> gatewayApiProvider;
@Autowired
@Qualifier("gatewayApiNacosPublisher")
private DynamicRulePublisher<List<ApiDefinitionEntity>> gatewayApiPublisher;
// ---------------- 改动 end ----------------
// 其余代码...
}
修改后的代码如下
@GetMapping("/list.json")
@AuthAction(AuthService.PrivilegeType.READ_RULE)
public Result<List<ApiDefinitionEntity>> queryApis(String app, String ip, Integer port) {
if (StringUtil.isEmpty(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
if (StringUtil.isEmpty(ip)) {
return Result.ofFail(-1, "ip can't be null or empty");
}
if (port == null) {
return Result.ofFail(-1, "port can't be null");
}
try {
// ---------------- 改动 start ----------------
// 1. 注释掉sentinelApiClient.fetchApis, 因为这是从内存中获取的
// List apis = sentinelApiClient.fetchApis(app, ip, port).get();
// 2. 引入我们自己的获取方法逻辑
// 获取规则
List<ApiDefinitionEntity> apis = gatewayApiProvider.getRules(app);
// 如果不为空就为规则设置上appName, ip, port, 然后保存到sentinel-dashboard中
if (CollUtil.isNotEmpty(apis)) {
for (ApiDefinitionEntity rule : apis) {
rule.setApp(app.trim());
rule.setIp(ip);
rule.setPort(port);
}
repository.saveAll(apis);
}
// ---------------- 改动 end ----------------
return Result.ofSuccess(apis);
} catch (Throwable throwable) {
logger.error("queryApis error:", throwable);
return Result.ofThrowable(-1, throwable);
}
}
修改后的代码如下
private boolean publishApis(String app, String ip, Integer port) {
List<ApiDefinitionEntity> apis = repository.findAllByMachine(MachineInfo.of(app, ip, port));
return sentinelApiClient.modifyApis(app, ip, port, apis);
}
/**
* 将网关API分组管理规则推送到nacos
* @param app 应用名
*/
// ---------------- 改动 end ----------------
private void publishApis(String app) {
List<ApiDefinitionEntity> apis = repository.findAllByApp(app);
try {
// 注意了, 这个是我们自定书写的重构方法, 没有返回值
gatewayApiPublisher.publish(app, apis);
} catch (Exception e) {
logger.warn("网关API管理规则推送nacos失败", e);
}
}
// ---------------- 改动 end ----------------
@PostMapping("/new.json")
@AuthAction(AuthService.PrivilegeType.WRITE_RULE)
public Result<ApiDefinitionEntity> addApi(HttpServletRequest request, @RequestBody AddApiReqVo reqVo) {
String app = reqVo.getApp();
if (StringUtil.isBlank(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
ApiDefinitionEntity entity = new ApiDefinitionEntity();
entity.setApp(app.trim());
String ip = reqVo.getIp();
if (StringUtil.isBlank(ip)) {
return Result.ofFail(-1, "ip can't be null or empty");
}
entity.setIp(ip.trim());
Integer port = reqVo.getPort();
if (port == null) {
return Result.ofFail(-1, "port can't be null");
}
entity.setPort(port);
// API名称
String apiName = reqVo.getApiName();
if (StringUtil.isBlank(apiName)) {
return Result.ofFail(-1, "apiName can't be null or empty");
}
entity.setApiName(apiName.trim());
// 匹配规则列表
List<ApiPredicateItemVo> predicateItems = reqVo.getPredicateItems();
if (CollectionUtils.isEmpty(predicateItems)) {
return Result.ofFail(-1, "predicateItems can't empty");
}
List<ApiPredicateItemEntity> predicateItemEntities = new ArrayList<>();
for (ApiPredicateItemVo predicateItem : predicateItems) {
ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity();
// 匹配模式
Integer matchStrategy = predicateItem.getMatchStrategy();
if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX).contains(matchStrategy)) {
return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy);
}
predicateItemEntity.setMatchStrategy(matchStrategy);
// 匹配串
String pattern = predicateItem.getPattern();
if (StringUtil.isBlank(pattern)) {
return Result.ofFail(-1, "pattern can't be null or empty");
}
predicateItemEntity.setPattern(pattern);
predicateItemEntities.add(predicateItemEntity);
}
entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities));
// 检查API名称不能重复
List<ApiDefinitionEntity> allApis = repository.findAllByMachine(MachineInfo.of(app.trim(), ip.trim(), port));
if (allApis.stream().map(o -> o.getApiName()).anyMatch(o -> o.equals(apiName.trim()))) {
return Result.ofFail(-1, "apiName exists: " + apiName);
}
Date date = new Date();
entity.setGmtCreate(date);
entity.setGmtModified(date);
try {
// ---------------- 改动 start ----------------
// 设置ID
// 获取所有规则
List<ApiDefinitionEntity> rules = gatewayApiProvider.getRules(entity.getApp());
// 如果不为空, 就获取最大的ID, 然后新添加的规则就设置为这个ID+1
if (CollUtil.isNotEmpty(rules)) {
Optional<ApiDefinitionEntity> apiRule = rules.stream()
.max(Comparator.comparingLong(ApiDefinitionEntity::getId));
entity.setId(apiRule.get().getId() + 1L);
}
// ---------------- 改动 end ----------------
} catch (Throwable throwable) {
logger.error("add gateway api error:", throwable);
return Result.ofThrowable(-1, throwable);
}
// ---------------- 改动 start ----------------
/*if (!publishApis(app, ip, port)) {
logger.warn("publish gateway apis fail after add");
}*/
publishApis(app);
// ---------------- 改动 end ----------------
return Result.ofSuccess(entity);
}
修改后的代码如下
@PostMapping("/delete.json")
@AuthAction(AuthService.PrivilegeType.DELETE_RULE)
public Result<Long> deleteApi(Long id) {
if (id == null) {
return Result.ofFail(-1, "id can't be null");
}
ApiDefinitionEntity oldEntity = repository.findById(id);
if (oldEntity == null) {
return Result.ofSuccess(null);
}
try {
repository.delete(id);
} catch (Throwable throwable) {
logger.error("delete gateway api error:", throwable);
return Result.ofThrowable(-1, throwable);
}
// ---------------- 改动 start ----------------
/*if (!publishApis(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) {
logger.warn("publish gateway apis fail after delete");
}*/
publishApis(oldEntity.getApp());
// ---------------- 改动 start ----------------
return Result.ofSuccess(id);
}
GatewayFlowRuleController类所在路径:src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java
需要修改的接口概览
URL | 方法名 | 请求方式 | 接口说明 |
---|---|---|---|
/gateway/flow/list.json | queryFlowRules | GET | 获取所有的网关流控规则列表 |
/gateway/flow/new.json | addFlowRule | POST | 新增网关流控规则 |
/gateway/flow/save.json | updateFlowRule | POST | 修改当前存在的网关流控规则 |
/gateway/flow/delete.json | deleteFlowRule | POST | 删除一条网关流控规则 |
// @Autowired
// private SentinelApiClient sentinelApiClient;
@Autowired
@Qualifier("gatewayFlowRulesNacosProvider")
private GatewayFlowRulesNacosProvider gatewayFlowProvider;
@Autowired
@Qualifier("gatewayFlowRulesNacosPublisher")
private DynamicRulePublisher<List<GatewayFlowRuleEntity>> gatewayFlowPublisher;
修改代码如下
@GetMapping("/list.json")
@AuthAction(AuthService.PrivilegeType.READ_RULE)
public Result<List<GatewayFlowRuleEntity>> queryFlowRules(String app, String ip, Integer port) {
if (StringUtil.isEmpty(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
if (StringUtil.isEmpty(ip)) {
return Result.ofFail(-1, "ip can't be null or empty");
}
if (port == null) {
return Result.ofFail(-1, "port can't be null");
}
try {
// ---------------- 改动 start ----------------
// List rules = sentinelApiClient.fetchGatewayFlowRules(app, ip, port).get();
List<GatewayFlowRuleEntity> rules = gatewayFlowProvider.getRules(app, ip, port);
if (CollUtil.isNotEmpty(rules)) {
for (GatewayFlowRuleEntity rule : rules) {
rule.setApp(app.trim());
rule.setIp(ip);
rule.setPort(port);
// interval: 统计时间窗口的大小, 如果没有设置默认就为 1
if (ObjUtil.isNull(rule.getInterval())) {
rule.setInterval(1L);
}
// intervalUnit: 统计时间窗口的时间单位, 如果没有设置, 那么就默认为秒
if (ObjUtil.isNull(rule.getIntervalUnit())) {
rule.setIntervalUnit(INTERVAL_UNIT_SECOND);
}
// controlBehavior:流量整型的控制效果,同限流规则的 controlBehavior 字段
// 目前支持快速失败和匀速排队两种模式,默认是快速失败
// 0-快速失败 1-匀速排队
if (ObjUtil.isNull(rule.getControlBehavior())) {
rule.setControlBehavior(0);
}
// burst:应对突发请求时额外允许的请求数目
if (ObjUtil.isNull(rule.getBurst())) {
rule.setBurst(0);
}
}
repository.saveAll(rules);
}
// ---------------- 改动 end ----------------
return Result.ofSuccess(rules);
} catch (Throwable throwable) {
logger.error("query gateway flow rules error:", throwable);
return Result.ofThrowable(-1, throwable);
}
}
修改代码如下
@PostMapping("/new.json")
@AuthAction(AuthService.PrivilegeType.WRITE_RULE)
public Result<GatewayFlowRuleEntity> addFlowRule(@RequestBody AddFlowRuleReqVo reqVo) {
String app = reqVo.getApp();
if (StringUtil.isBlank(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity();
entity.setApp(app.trim());
String ip = reqVo.getIp();
if (StringUtil.isBlank(ip)) {
return Result.ofFail(-1, "ip can't be null or empty");
}
entity.setIp(ip.trim());
Integer port = reqVo.getPort();
if (port == null) {
return Result.ofFail(-1, "port can't be null");
}
entity.setPort(port);
// API类型, Route ID或API分组
Integer resourceMode = reqVo.getResourceMode();
if (resourceMode == null) {
return Result.ofFail(-1, "resourceMode can't be null");
}
if (!Arrays.asList(RESOURCE_MODE_ROUTE_ID, RESOURCE_MODE_CUSTOM_API_NAME).contains(resourceMode)) {
return Result.ofFail(-1, "invalid resourceMode: " + resourceMode);
}
entity.setResourceMode(resourceMode);
// API名称
String resource = reqVo.getResource();
if (StringUtil.isBlank(resource)) {
return Result.ofFail(-1, "resource can't be null or empty");
}
entity.setResource(resource.trim());
// 针对请求属性
GatewayParamFlowItemVo paramItem = reqVo.getParamItem();
if (paramItem != null) {
GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity();
entity.setParamItem(itemEntity);
// 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie
Integer parseStrategy = paramItem.getParseStrategy();
if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER
, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy);
}
itemEntity.setParseStrategy(paramItem.getParseStrategy());
// 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填
if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
// 参数名称
String fieldName = paramItem.getFieldName();
if (StringUtil.isBlank(fieldName)) {
return Result.ofFail(-1, "fieldName can't be null or empty");
}
itemEntity.setFieldName(paramItem.getFieldName());
}
String pattern = paramItem.getPattern();
// 如果匹配串不为空,验证匹配模式
if (StringUtil.isNotEmpty(pattern)) {
itemEntity.setPattern(pattern);
Integer matchStrategy = paramItem.getMatchStrategy();
if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) {
return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy);
}
itemEntity.setMatchStrategy(matchStrategy);
}
}
// 阈值类型 0-线程数 1-QPS
Integer grade = reqVo.getGrade();
if (grade == null) {
return Result.ofFail(-1, "grade can't be null");
}
if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) {
return Result.ofFail(-1, "invalid grade: " + grade);
}
entity.setGrade(grade);
// QPS阈值
Double count = reqVo.getCount();
if (count == null) {
return Result.ofFail(-1, "count can't be null");
}
if (count < 0) {
return Result.ofFail(-1, "count should be at lease zero");
}
entity.setCount(count);
// 间隔
Long interval = reqVo.getInterval();
if (interval == null) {
return Result.ofFail(-1, "interval can't be null");
}
if (interval <= 0) {
return Result.ofFail(-1, "interval should be greater than zero");
}
entity.setInterval(interval);
// 间隔单位
Integer intervalUnit = reqVo.getIntervalUnit();
if (intervalUnit == null) {
return Result.ofFail(-1, "intervalUnit can't be null");
}
if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) {
return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit);
}
entity.setIntervalUnit(intervalUnit);
// 流控方式 0-快速失败 2-匀速排队
Integer controlBehavior = reqVo.getControlBehavior();
if (controlBehavior == null) {
return Result.ofFail(-1, "controlBehavior can't be null");
}
if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) {
return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior);
}
entity.setControlBehavior(controlBehavior);
if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) {
// 0-快速失败, 则Burst size必填
Integer burst = reqVo.getBurst();
if (burst == null) {
return Result.ofFail(-1, "burst can't be null");
}
if (burst < 0) {
return Result.ofFail(-1, "invalid burst: " + burst);
}
entity.setBurst(burst);
} else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) {
// 1-匀速排队, 则超时时间必填
Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs();
if (maxQueueingTimeoutMs == null) {
return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null");
}
if (maxQueueingTimeoutMs < 0) {
return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs);
}
entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs);
}
Date date = new Date();
entity.setGmtCreate(date);
entity.setGmtModified(date);
try {
entity = repository.save(entity);
} catch (Throwable throwable) {
logger.error("add gateway flow rule error:", throwable);
return Result.ofThrowable(-1, throwable);
}
// if (!publishRules(app, ip, port)) {
// logger.warn("publish gateway flow rules fail after add");
// }
try {
List<GatewayFlowRuleEntity> rules = gatewayFlowProvider.getRules(entity.getApp());
if (CollectionUtil.isNotEmpty(rules)) {
Optional<GatewayFlowRuleEntity> gatewayFlowRule = rules.stream()
.max(Comparator.comparingLong(GatewayFlowRuleEntity::getId));
entity.setId(gatewayFlowRule.get().getId() + 1L);
}
} catch (Exception e) {
logger.warn("get gateway flow rules error:", e);
}
return Result.ofSuccess(entity);
}
@PostMapping("/save.json")
@AuthAction(AuthService.PrivilegeType.WRITE_RULE)
public Result<GatewayFlowRuleEntity> updateFlowRule(@RequestBody UpdateFlowRuleReqVo reqVo) {
String app = reqVo.getApp();
if (StringUtil.isBlank(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
Long id = reqVo.getId();
if (id == null) {
return Result.ofFail(-1, "id can't be null");
}
GatewayFlowRuleEntity entity = repository.findById(id);
if (entity == null) {
return Result.ofFail(-1, "gateway flow rule does not exist, id=" + id);
}
// 针对请求属性
GatewayParamFlowItemVo paramItem = reqVo.getParamItem();
if (paramItem != null) {
GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity();
entity.setParamItem(itemEntity);
// 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie
Integer parseStrategy = paramItem.getParseStrategy();
if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER
, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy);
}
itemEntity.setParseStrategy(paramItem.getParseStrategy());
// 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填
if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
// 参数名称
String fieldName = paramItem.getFieldName();
if (StringUtil.isBlank(fieldName)) {
return Result.ofFail(-1, "fieldName can't be null or empty");
}
itemEntity.setFieldName(paramItem.getFieldName());
}
String pattern = paramItem.getPattern();
// 如果匹配串不为空,验证匹配模式
if (StringUtil.isNotEmpty(pattern)) {
itemEntity.setPattern(pattern);
Integer matchStrategy = paramItem.getMatchStrategy();
if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) {
return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy);
}
itemEntity.setMatchStrategy(matchStrategy);
}
} else {
entity.setParamItem(null);
}
// 阈值类型 0-线程数 1-QPS
Integer grade = reqVo.getGrade();
if (grade == null) {
return Result.ofFail(-1, "grade can't be null");
}
if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) {
return Result.ofFail(-1, "invalid grade: " + grade);
}
entity.setGrade(grade);
// QPS阈值
Double count = reqVo.getCount();
if (count == null) {
return Result.ofFail(-1, "count can't be null");
}
if (count < 0) {
return Result.ofFail(-1, "count should be at lease zero");
}
entity.setCount(count);
// 间隔
Long interval = reqVo.getInterval();
if (interval == null) {
return Result.ofFail(-1, "interval can't be null");
}
if (interval <= 0) {
return Result.ofFail(-1, "interval should be greater than zero");
}
entity.setInterval(interval);
// 间隔单位
Integer intervalUnit = reqVo.getIntervalUnit();
if (intervalUnit == null) {
return Result.ofFail(-1, "intervalUnit can't be null");
}
if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) {
return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit);
}
entity.setIntervalUnit(intervalUnit);
// 流控方式 0-快速失败 2-匀速排队
Integer controlBehavior = reqVo.getControlBehavior();
if (controlBehavior == null) {
return Result.ofFail(-1, "controlBehavior can't be null");
}
if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) {
return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior);
}
entity.setControlBehavior(controlBehavior);
if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) {
// 0-快速失败, 则Burst size必填
Integer burst = reqVo.getBurst();
if (burst == null) {
return Result.ofFail(-1, "burst can't be null");
}
if (burst < 0) {
return Result.ofFail(-1, "invalid burst: " + burst);
}
entity.setBurst(burst);
} else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) {
// 2-匀速排队, 则超时时间必填
Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs();
if (maxQueueingTimeoutMs == null) {
return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null");
}
if (maxQueueingTimeoutMs < 0) {
return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs);
}
entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs);
}
Date date = new Date();
entity.setGmtModified(date);
// ---------------- 改动 start ----------------
try {
// 设置ID
List<GatewayFlowRuleEntity> rules = gatewayFlowProvider.getRules(app, ip, port);
if (CollectionUtil.isNotEmpty(rules)) {
Optional<GatewayFlowRuleEntity> gatewayFlowRule = rules.stream()
.max(Comparator.comparingLong(GatewayFlowRuleEntity::getId));
entity.setId(gatewayFlowRule.get().getId() + 1L);
}
entity = repository.save(entity);
} catch (Throwable throwable) {
logger.error("add gateway flow rule error:", throwable);
return Result.ofThrowable(-1, throwable);
}
// if (!publishRules(app, entity.getIp(), entity.getPort())) {
// logger.warn("publish gateway flow rules fail after update");
// }
publishRules(app);
// ---------------- 改动 end ----------------
return Result.ofSuccess(entity);
}
/**
* 将网关限流规则推送到nacos中
* @param app application的应用程序名称
*/
private void publishRules(String app) {
List<GatewayFlowRuleEntity> rules = repository.findAllByApp(app);
try {
gatewayFlowPublisher.publish(app, rules);
} catch (Exception e) {
logger.warn("publish gateway flow rules fail", e);;
}
}
修改代码如下
@PostMapping("/save.json")
@AuthAction(AuthService.PrivilegeType.WRITE_RULE)
public Result<GatewayFlowRuleEntity> updateFlowRule(@RequestBody UpdateFlowRuleReqVo reqVo) {
String app = reqVo.getApp();
if (StringUtil.isBlank(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
Long id = reqVo.getId();
if (id == null) {
return Result.ofFail(-1, "id can't be null");
}
GatewayFlowRuleEntity entity = repository.findById(id);
if (entity == null) {
return Result.ofFail(-1, "gateway flow rule does not exist, id=" + id);
}
// 针对请求属性
GatewayParamFlowItemVo paramItem = reqVo.getParamItem();
if (paramItem != null) {
GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity();
entity.setParamItem(itemEntity);
// 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie
Integer parseStrategy = paramItem.getParseStrategy();
if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER
, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy);
}
itemEntity.setParseStrategy(paramItem.getParseStrategy());
// 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填
if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) {
// 参数名称
String fieldName = paramItem.getFieldName();
if (StringUtil.isBlank(fieldName)) {
return Result.ofFail(-1, "fieldName can't be null or empty");
}
itemEntity.setFieldName(paramItem.getFieldName());
}
String pattern = paramItem.getPattern();
// 如果匹配串不为空,验证匹配模式
if (StringUtil.isNotEmpty(pattern)) {
itemEntity.setPattern(pattern);
Integer matchStrategy = paramItem.getMatchStrategy();
if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) {
return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy);
}
itemEntity.setMatchStrategy(matchStrategy);
}
} else {
entity.setParamItem(null);
}
// 阈值类型 0-线程数 1-QPS
Integer grade = reqVo.getGrade();
if (grade == null) {
return Result.ofFail(-1, "grade can't be null");
}
if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) {
return Result.ofFail(-1, "invalid grade: " + grade);
}
entity.setGrade(grade);
// QPS阈值
Double count = reqVo.getCount();
if (count == null) {
return Result.ofFail(-1, "count can't be null");
}
if (count < 0) {
return Result.ofFail(-1, "count should be at lease zero");
}
entity.setCount(count);
// 间隔
Long interval = reqVo.getInterval();
if (interval == null) {
return Result.ofFail(-1, "interval can't be null");
}
if (interval <= 0) {
return Result.ofFail(-1, "interval should be greater than zero");
}
entity.setInterval(interval);
// 间隔单位
Integer intervalUnit = reqVo.getIntervalUnit();
if (intervalUnit == null) {
return Result.ofFail(-1, "intervalUnit can't be null");
}
if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) {
return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit);
}
entity.setIntervalUnit(intervalUnit);
// 流控方式 0-快速失败 2-匀速排队
Integer controlBehavior = reqVo.getControlBehavior();
if (controlBehavior == null) {
return Result.ofFail(-1, "controlBehavior can't be null");
}
if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) {
return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior);
}
entity.setControlBehavior(controlBehavior);
if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) {
// 0-快速失败, 则Burst size必填
Integer burst = reqVo.getBurst();
if (burst == null) {
return Result.ofFail(-1, "burst can't be null");
}
if (burst < 0) {
return Result.ofFail(-1, "invalid burst: " + burst);
}
entity.setBurst(burst);
} else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) {
// 2-匀速排队, 则超时时间必填
Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs();
if (maxQueueingTimeoutMs == null) {
return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null");
}
if (maxQueueingTimeoutMs < 0) {
return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs);
}
entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs);
}
Date date = new Date();
entity.setGmtModified(date);
try {
entity = repository.save(entity);
} catch (Throwable throwable) {
logger.error("add gateway flow rule error:", throwable);
return Result.ofThrowable(-1, throwable);
}
// ---------------- 改动 start ----------------
// if (!publishRules(app, entity.getIp(), entity.getPort())) {
// logger.warn("publish gateway flow rules fail after update");
// }
publishRules(app);
// ---------------- 改动 end ----------------
return Result.ofSuccess(entity);
}
修改代码如下
@PostMapping("/delete.json")
@AuthAction(AuthService.PrivilegeType.DELETE_RULE)
public Result<Long> deleteFlowRule(Long id) {
if (id == null) {
return Result.ofFail(-1, "id can't be null");
}
GatewayFlowRuleEntity oldEntity = repository.findById(id);
if (oldEntity == null) {
return Result.ofSuccess(null);
}
try {
repository.delete(id);
} catch (Throwable throwable) {
logger.error("delete gateway flow rule error:", throwable);
return Result.ofThrowable(-1, throwable);
}
// ---------------- 改动 start ----------------
// if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) {
// logger.warn("publish gateway flow rules fail after delete");
// }
publishRules(oldEntity.getApp());
// ---------------- 改动 end ----------------
return Result.ofSuccess(id);
}
gateway集成redis
Spring Cloud Gateway 限流适配多规则的解决方案)
限流10万QPS、跨域、过滤器、令牌桶算法-网关Gateway内容都在这儿)
面试必备:四种经典限流算法讲解)
SpringCloudGateway源码(四)限流组件)
[分布式限流方案(gateway限流,redis+lua实现限流,nginx限流)](https://zhuanlan.zhihu.com/p/571518397#:~:text=实现方案 :Guava RateLimiter限流 Guava RateLimiter是一个谷歌提供的限流,其基于令牌桶算法,比较适用于单实例的系统。 限流具体实现,网关限流: Spring Cloud Gateway中提供了RequestRateLimiterGatewayFilterFactory类,这个是基于令牌桶实现的。 它内置RedisReteLimiter%2C依赖于Redis存储限流配置和统计数据,我们也可以通过继承 org.springframework.cloud.gateway.filter.ratelimit.AbstractRateLimiter 或者是实现)
gateway集成sentinel
阿里巴巴开源限流系统 Sentinel 全解析)
Spring Cloud Gateway 限流实战,终于有人写清楚了)
sentinel官方文档)
Spring Cloud Alibaba:Sentinel实现熔断与限流)
SpringCloudAlibaba全网最全讲解6️⃣之Sentinel)
Sentinel系列源码)
阿里sentinel与springboot整合实践——根据request信息限流
Sentinel规则持久化(1.8.+版)
Sentinel规则持久化到Nacos及规则数据双向同步
Sentinel1.8.6 Gateway网关流控+动态Nacos数据源双向持久化