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

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

相关推荐
Nejosi_念旧2 小时前
解读 Go 中的 constraints包
后端·golang·go
风无雨2 小时前
GO 启动 简单服务
开发语言·后端·golang
Hellyc2 小时前
用户查询优惠券之缓存击穿
java·redis·缓存
小明的小名叫小明2 小时前
Go从入门到精通(19)-协程(goroutine)与通道(channel)
后端·golang
斯普信专业组2 小时前
Go语言包管理完全指南:从基础到最佳实践
开发语言·后端·golang
一只叫煤球的猫4 小时前
【🤣离谱整活】我写了一篇程序员掉进 Java 异世界的短篇小说
java·后端·程序员
鼠鼠我捏,要死了捏4 小时前
缓存穿透与击穿多方案对比与实践指南
redis·缓存·实践指南
你的人类朋友5 小时前
🫏光速入门cURL
前端·后端·程序员
aramae7 小时前
C++ -- STL -- vector
开发语言·c++·笔记·后端·visual studio
洁辉7 小时前
Spring Cloud 全栈指南:构建云原生微服务的终极武器
spring cloud·微服务·云原生