重学SpringBoot3-集成Redis(七)之分布式限流

重学SpringBoot3-集成Redis(七)之分布式限流

CoderJia 19 2024-10-08

在分布式系统中,流量控制是一项关键的任务,特别是当我们需要应对瞬时的大流量请求时。分布式限流是一种有效的方式,可以防止系统被突发的请求击垮。在这篇文章中,将介绍如何通过 RedisSpring Boot 3 实现分布式限流。

1. 什么是分布式限流?

限流的目的是控制一定时间窗口内的请求数量,确保系统稳定运行。分布式限流的核心在于它可以在多台服务器或多个节点上,对所有的请求进行全局控制。通过限流,我们可以:

  • 防止系统过载
  • 保护关键资源
  • 提高系统的稳定性和可用性

Redis 作为一个内存存储系统,具有高性能、分布式、可扩展的特点,非常适合用于实现分布式限流。

2. 常见的限流算法

在介绍具体实现之前,我们先了解一些常用的限流算法:

  1. 固定窗口算法:在一个固定的时间窗口内,限制请求次数。
  2. 滑动窗口算法:相比固定窗口算法,滑动窗口更平滑,可以在时间窗口内动态更新请求。
  3. 令牌桶算法:根据流量产生令牌,请求只能在获取到令牌后才可以通过。
  4. 漏桶算法:以固定速率处理请求,超出速率的请求被丢弃。

首先将以 固定窗口 为例,展示如何通过 Redis 实现限流。

3. Redis 分布式限流实现

3.1. 添加依赖

首先,确保项目中已经引入了 Redis 依赖,详细参考重学SpringBoot3-集成Redis(一)基本使用。在 pom.xml 文件中添加以下依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

3.2. 配置 Redis 连接

application.yml 中配置 Redis 的基本连接信息:

spring:
  data:
    redis:
      host: localhost
      port: 6379            # Redis 端口
      password:             # 如果有密码可以在这里配置
      lettuce:
        pool:
          max-active: 100    # 最大并发连接数
          max-idle: 50       # 最大空闲连接数
          min-idle: 10       # 最小空闲连接数

3.3. 编写限流逻辑

我们可以使用 Redis 的 INCR 命令结合 EXPIRE 来实现限流功能。每当有请求时,先检查当前时间窗口内的请求次数,如果超过限制则拒绝请求。

package com.coderjia.boot310redis.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Duration;

/**
 * @author CoderJia
 * @create 2024/10/7 上午 11:14
 * @Description
 **/
@Component
public class RateLimiter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 限制 10 秒内最多允许 5 次请求,可放到配置文件中
    private static final int LIMIT = 5;
    private static final int TIME_WINDOW = 10; // 单位:秒

    public boolean isAllowed(String userId) {
        String key = "rate:limit:" + userId;
        Long currentCount = redisTemplate.opsForValue().increment(key);

        if (currentCount == 1) {
            // 设置 key 的过期时间为 10 秒
            redisTemplate.expire(key, Duration.ofSeconds(TIME_WINDOW));
        }

        // 超过限制
        return currentCount <= LIMIT;
    }
}

在这个类中:

  • 我们使用 INCR 命令来对某个键进行自增操作。
  • 当第一次自增时,我们使用 expire 设置键的过期时间,即时间窗口的长度(如 10 秒)。
  • 如果某个用户在这个时间窗口内的请求次数超过了限制值,则返回 false,表示请求被拒绝。

3.4. 编写控制器测试限流

为了测试限流功能,我们创建一个简单的控制器,当用户访问时,检查其是否被限流。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RateLimitController {

    @Autowired
    private RateLimiter rateLimiter;

    @GetMapping("/api/request")
    public String handleRequest(@RequestParam String userId) {
        boolean allowed = rateLimiter.isAllowed(userId);

        if (allowed) {
            return "Request allowed for user: " + userId;
        } else {
            return "Request limit exceeded for user: " + userId;
        }
    }
}

现在,当用户连续访问 http://localhost:8080/api/request?userId=1 时,系统会根据设定的规则,决定是否允许请求。如下图,连续5次请求之后,之后再请求将会被限制访问,直到10s之后,进入到下一个固定窗口,可以再访问5次,之后触发限流。

限流效果

4. 限流算法的改进

在实际场景中,我们可以根据业务需求选择不同的限流算法。例如:

  1. 滑动窗口限流:通过记录每个请求的时间戳来实现更精确的限流控制。
  2. 令牌桶限流:可以动态调整请求速率,允许一定程度的突发流量。

如果需要更多的定制化功能,可以借助 Redisson 等工具,它提供了开箱即用的分布式限流功能,能够更加优雅地处理复杂的限流场景。

Redisson 是基于 Redis 的 Java 客户端,它提供了多种高级功能,包括分布式锁、信号量、限流等。利用 Redisson 实现滑动窗口限流,我们可以避免直接通过 Redis 的 INCREXPIRE 命令来手动操作,而是使用其提供的高层 API 实现更加精确的流量控制。

4.1. 什么是滑动窗口限流?

滑动窗口限流是一种动态的限流策略,它将时间划分为多个小窗口,并在多个窗口中统计请求次数,从而实现平滑的流量控制。相比于固定窗口限流,滑动窗口限流不会因为时间窗口的边界问题导致突发流量通过,而是根据请求的时间动态调整。

4.2. 使用 Redisson 实现滑动窗口限流

4.2.1. 引入依赖

详细配置可参考重学SpringBoot3-集成Redis(四)之Redisson ,需要在 pom.xml 文件中引入 Redisson 的依赖:

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.23.4</version>
        </dependency>

4.2.2. 配置 Redisson

配置 Redisson 的基本信息,在 application.yml 文件中进行 Redis 的连接配置:

spring:
  redis:
    redisson:
      config: |
        singleServerConfig:
          address: redis://1.94.26.81:6379   # Redis 连接地址,前缀为 redis://
          password: redis123456              # 如果 Redis 需要密码认证,则填写密码
          timeout: 3000                      # 命令执行超时时间(毫秒)

4.2.3. 滑动窗口限流的实现

Redisson 提供了 RateLimiter(限流器)来控制请求频率。RateLimiter 支持滑动窗口限流算法,我们可以通过它来控制一定时间内的请求次数。

package com.coderjia.boot310redis.config;

import jakarta.annotation.PostConstruct;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author CoderJia
 * @create 2024/10/7 下午 12:10
 * @Description
 **/
@Component
public class SlidingWindowRateLimiter {

    @Autowired
    private RedissonClient redissonClient;

    private RRateLimiter rateLimiter;

    @PostConstruct
    public void init() {
        // 创建一个滑动窗口限流器
        rateLimiter = redissonClient.getRateLimiter("rrateLimiter");

        // 每 1 秒最多允许 5 次请求
        boolean b = rateLimiter.trySetRate(RateType.OVERALL, 5, 1, RateIntervalUnit.SECONDS);
        // 查看是否设置成功
        if (b){
            System.out.println("限流器设置成功");
        }else {
            System.out.println("限流器设置失败");
        }
    }

    public boolean tryAcquire() {
        // 尝试获取一个令牌
        return rateLimiter.tryAcquire(1);
    }
}

在这个代码中:

  • rateLimiter.trySetRate() 设置限流规则,每人每 1 秒最多允许 5 次请求
  • RateType.PER_CLIENT 设置限制同一用户,如果限制全部用户请求,可改为 RateType.OVERALL
  • rateLimiter.tryAcquire(1) 尝试获取一个令牌,如果成功获取,则返回 true,表示请求通过;如果失败,则返回 false,表示请求被限流。

4.2.4. 控制器测试限流

我们可以通过一个简单的控制器来测试滑动窗口限流:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RateLimitController {

    @Autowired
    private SlidingWindowRateLimiter rateLimiter;

    @GetMapping("/api/request")
    public String handleRequest() {
        boolean allowed = rateLimiter.tryAcquire();

        if (allowed) {
            return "Request allowed";
        } else {
            return "Request limit exceeded";
        }
    }
}

当我们访问 http://localhost:8080/api/request 时,系统会检查是否符合限流条件,从而决定是否允许请求。

这里需要借助 apifox 进行压测:

模拟单用户并发请求

4.2.5. 单用户限制

首先是单用户限制每秒钟访问5次,单用户连续请求100次,报告显示12s时间成功请求了60次,达到了我们的效果。

单用户限制

4.2.6. 全部请求限制

下面模拟20用户连续请求1分钟,下面报告显示1分钟内,请求了996次,由于设置的1秒内运行请求5次,所有成功率60*5/996约等于30%,考虑接口耗时,和报告的失败率近似。

全部请求限制

4.3. 优势

  1. 高精度限流:滑动窗口可以更精确地统计请求数,相较于固定窗口限流,它可以避免窗口边界导致的瞬时突发请求超出限制的情况。
  2. 自动管理:Redisson 自带的 RateLimiter 限流器无需手动维护 Redis 键值过期时间,它会自动处理这些细节,大大简化了限流的实现。

5. 总结

通过 Redis 实现的分布式限流,具有实现简单、性能高、易于扩展的优势。它能够帮助我们应对瞬时的大流量请求,保护系统免于过载。同时,结合不同的限流算法,我们可以根据具体的业务需求,设计灵活、精准的限流策略。

如果你在项目中遇到类似的限流需求,Redis 是一个很好的选择。希望这篇文章能够帮助你了解如何使用 Spring Boot 3 和 Redis 实现分布式限流。如果你有任何疑问或建议,欢迎留言讨论!