Spring Cloud Gateway 自定义分布式限流

这篇文章主要分享使用Redisson在Spring Cloud Gateway 中实现分布式自定义限流

为啥不使用Sentinel?

想到分布式限流组件,大家很容易想到Sentinel,但是其实有很多局限性,首先Sentinel的流量控制有两个维度的控制:并发线程数QPS,它们都是针对某个具体的接口来设置的

Sentinel的限流原理主要是通过统计系统的QPS(即每秒请求数量)和并发量来控制系统的流量,从而达到限流的目的。

    其实之前分享过Sentinel限流的应用,大家感兴趣的话可以先看下了解下。

可以看出目前不支持自定义时间窗口,与分布式限流,以及自定义规则限流,比如 IP地址,用户等维度限流功能

常用限流方案比对

在 Spring Cloud Gateway 中实现分布式限流时,​​Redisson 方案​ ​与原生 ​​RequestRateLimiter​​ 是两种主流方案,以下是核心对比与选型建议:

​特性​ ​Redisson 方案​ ​RequestRateLimiter (原生)​
​分布式支持​ ✅ 基于 Redis 集群,天然分布式 ✅ 基于 Redis,依赖 Lua 脚本实现分布式
​时间窗口灵活性​ ✅ ​​秒/分/时/天级自由配置​ ​(RateIntervalUnit ❌ 仅支持秒级窗口(需扩展)
​限流算法​ ✅ 令牌桶算法(允许突发流量) ✅ 令牌桶算法(固定窗口)
​动态规则更新​ ✅ 通过 trySetRate()​实时调整​​规则 ❌ 需重启服务或手动刷新配置
​限流维度​ ✅ 支持 IP、用户、API 等​​多维度组合键​ ✅ 需自定义 KeyResolver 实现
​依赖复杂度​ 需引入 Redisson 客户端 仅需 Spring Data Redis Reactive

Redisson 核心优势​​:

  • ​灵活时间窗口​​:支持分钟/小时级限流(如 1 分钟 100 次请求)

  • ​动态生效​​:限流规则可运行时热更新;

  • ​精细控制​ ​:提供 RateType.PER_CLIENT(按客户端限流)和 RateType.OVERALL(全局限流)

Redisson 分布式限流总体设计

处理流程

  1. 客户端请求到达 Spring Cloud Gateway。
  2. Gateway 根据配置的 KeyResolver 解析出限流的键(例如:IP、用户ID、API等)。
  3. 根据路由地址,从动态配置中获取限流规则(例如:每秒100次)。
  4. 使用 Redis 执行 Lua 脚本(保证原子性)进行限流计数和判断。
  5. 如果未超过阈值,请求放行;否则返回429(Too Many Requests)。
  6. 动态配置管理:通过管理后台更新限流规则,并同步到所有网关节点。

redission令牌桶限流算法

系统以固定速率(如每秒 100 个)向桶中添加令牌。例如:

java 复制代码
rateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.SECONDS);
  • rate:令牌生成速率(100 个/秒)
  • interval + unit:时间窗口(1 秒)

Redisson 分布式限流的具体代码实现

引入依赖版本

版本兼容性

这边在引入Redisson版本的,一定要注意与gateway版本兼容还有Redis服务版本问题,具体可以参考下,优先根据 Spring Boot 版本选择 Redisson,再匹配 Spring Cloud Gateway

  • Spring Boot 2.4.x → Redisson 3.17.x
  • Spring Boot 2.7.x → Redisson 3.23.x
  • Spring Boot 3.2.x → Redisson 3.27.x+
场景​ ​Spring Cloud Gateway​ ​Redisson​ ​Spring Boot​ ​Redis Server​
传统项目(兼容旧版) 2.2.2.RELEASE 3.17.7 2.4.13 6.x
新项目(推荐) 4.2.0 3.27.1 3.2.4 7.x

我这边与gateway版本兼容问题 版本是2.1.1.RELEASE,引入的redisson版本是3.13.6,大家可以做个参考。

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    <version>2.1.1.RELEASE</version>
</dependency>
xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

初始化 RedissonClient

大家注意下这里 config.useClusterServers() 要根据自己的redis服务器选择不同的配置

java 复制代码
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class RedissonConfig {

    @Value("${redis.hosts}")
    private String redisHost;
    @Value("${redis.ports}")
    private int redisPort;
    @Value("${redis.password}")
    private String redisPassword;
    @Value("${redis.timeout}")
    private int timeout;


    @Bean(destroyMethod ="shutdown")
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useClusterServers()
                .addNodeAddress("redis://" +redisHost+ ":" + redisPort)
                .setPassword(redisPassword)
                .setConnectTimeout(timeout);
        return Redisson.create(config);
    }
}

规则存储

规则默认存储在mysql数据库中,就是一个很简单的结构,可以根据自己需求扩展

sql 复制代码
CREATE TABLE `limit_rules` (
  `id` bigint(20) NOT NULL,
  `request_user` varchar(64) DEFAULT NULL COMMENT '请求用户',
  `request_ip` varchar(64) DEFAULT NULL COMMENT '请求IP',
  `request_url` varchar(255) NOT NULL COMMENT '请求地址',
  `limit_count` int(11) NOT NULL COMMENT '限制数量',
  `window_size` int(11) NOT NULL COMMENT '时间窗口/秒',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `uni_request_url` (`request_url`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='分布式限流规则表';

网关定时任务,每隔30s拉取最新的规则数据存取在内存中

java 复制代码
@Service
public class RateLimitRuleService {
    @Autowired
    private RateLimitRuleRepository ruleRepository;
    private final Map<String, RateLimitRule> ruleCache = new ConcurrentHashMap<>();

    // 每30秒刷新规则缓存
    @Scheduled(fixedRate = 30000)
    public void refreshRules() {
        ruleRepository.findAll().forEach(rule -> 
            ruleCache.put(rule.getRouteId() + ":" + rule.getKeyExpression(), rule)
        );
    }
}

自定义限流过滤器​

这里有点需要注意,规则删除之后,一点要清除redis中附属的key的计数数据

java 复制代码
@Component
public class RedissonRateLimitFilter implements GlobalFilter {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RateLimitRuleService ruleService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String key = resolveKey(exchange); 
        // 1. 查询动态规则
        RateLimitRule rule = ruleService.getRule( key);
        //这里规则逻辑就省略了,大家自己根据自己规则设置
        if (rule == null) {
            return chain.filter(exchange); // 无规则则放行
        }
        if(rule.isEnabled()){
            // 如果规则被标记为删除,则直接返回
            log.error("RedissonRateLimitFilter rule for path {} has been deleted", path);
            this.destroyRateLimiter(key, path);
            return chain.filter(exchange);
        }

        // 2. 初始化Redisson限流器
        RRateLimiter rateLimiter = redissonClient.getRateLimiter("rate_limit:" + rule.getId() + ":" + key);
        rateLimiter.trySetRateAsync(
                RateType.OVERALL,
                rule.getLimitCount(),
                rule.getWindowSize(),
                RateIntervalUnit.SECONDS
        );

        // 4. 尝试获取令牌
        if (rateLimiter.tryAcquire()) {
            return chain.filter(exchange); // 放行
        } else {
            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            return exchange.getResponse().setComplete(); // 返回429
        }
    }
    
    public void destroyRateLimiter(String key,String requestUrl) {
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
        // 1. 删除主配置
        rateLimiter.delete();
        // 2. 删除附属 Key
        this.deleteAuxiliaryKeys(key);
        //3. 删除数据库中规则
        ruleService.deleteRule(requestUrl);
    }


    private void deleteAuxiliaryKeys(String key) {
        String valueKey = key + ":value";
        String permitsKey = key + ":permits";
        // 批量异步删除
        RBatch batch = redissonClient.createBatch();
        batch.getBucket(valueKey).deleteAsync();
        batch.getScoredSortedSet(permitsKey).deleteAsync();
        batch.execute();
    }

}

以上就是相关的代码,可以看到使用redisson还是很简单的就可以实现分布式限流的功能

压测数据

最后我们来看下压测相关数据,看下对性能影响如何?

对接口进行对照压测,线程组配置如下

1.无任何配置,吞吐 113/s

2.对接口进行限流配置,但是配置远大于请求数据,吞吐113.3/s

3.对接口进行限流配置,但是配置远小于请求数据,大部分接口限流,吞吐115.3/s

    因为压测接口没有任何逻辑,直接返回,所以看上去没有实际的差距,这里也懒得再重新压测了,感兴趣的可以自己再压测下相关业务接口,但是实际我们上线了生产环境一个月了,目前来看对于性能影响来看很小

相关推荐
间彧2 分钟前
Kubernetes的Pod与Docker Compose中的服务在概念上有何异同?
后端
间彧5 分钟前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧11 分钟前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧12 分钟前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧13 分钟前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧18 分钟前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧23 分钟前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang1 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构
睡前要喝豆奶粉1 小时前
在.NET Core Web Api中使用redis
redis·c#·.netcore
草明2 小时前
Go 的 IO 多路复用
开发语言·后端·golang