说说常见的限流算法及如何使用Redisson实现多机限流

本文介绍了四种常见的限流算法及其实现方式。固定窗口限流简单但存在临界突刺问题;滑动窗口通过时间片滑动解决突刺问题,但滑动单位选择困难;漏桶算法以固定速率处理请求,能削峰缓冲但不够灵活;令牌桶算法允许突发流量,并发性能更好但时间单位选择仍需考量。实现层面,单机限流可使用Guava的RateLimiter,分布式限流推荐Redisson或网关层工具如Sentinel。提供了Redisson的配置示例和两种限流设置方式,建议采用基于Duration的新API实现更简洁的限流控制。

限流算法介绍

1、固定窗口限流

在单位时间内允许部分操作。

就比如,单位时间(窗口)为1小时,一小时内只能有100个用户能访问,如果在一小时内就已经达到了次数上限100,那么这时不论来多少请求都会被拒绝掉,也就是说1小时内设定了多少次就是只能有多少次,只有到1小时才会重置次数。但是它有一个突刺问题,例如在前59分钟都没有一个用户进行访问,在59分钟的时候100个用户同时访问,然后1小时01分时候又有100个用户都访问了,那就是2分钟有200个用户同时访问,可能瞬间通过200个请求,在两个窗口的临界突刺,服务器可能就会有流量高峰危险。

优点:实现简单

缺点:突刺问题

2、滑动窗口限流

在单位时间内允许部分操作,不过这个单位时间是滑动的,需要指定一个滑动时间单位(时间片)。

**滑动窗口解决了固定窗口的突刺问题。**滑动窗口是持续滑动,逐时间片淘汰旧数据的过程,它不会像固定窗口一样在固定的时间点重置次数,而是持续淘汰旧数据,因此能更精确地控制任意连续内的请求速率。

比如,单位时间(窗口)是1小时,指定滑动单位是1分钟,那么在59分钟时它的限流窗口是59分~1小时59分,经过2分钟它的限流窗口就是1小时01分~2小时01分,这个时间段还是就只接收100个请求,如果有超过第100个的请求访问,超过的访问请求都会被拒绝掉;如果每2分钟都有1个旧的请求数据淘汰,经过2分钟这时只有99个数据就可以允许有1个新的请求进入。

优点:解决突刺问题

缺点:滑动窗口的限流效果与滑动单位有关,滑动单位小效果好,不过往往很难选到一个合适的滑动单位

3、漏桶算法限流(推荐)

请求桶大小固定,以固定的速率处理请求,当请求桶里面满了之后会拒绝请求。

漏桶算法限流的基本原理为:水(对应请求)从进水口进入到漏桶里,漏桶以一定的速度出水(请求放行),当水流入速度过大,桶内的总水量大于桶容量会直接溢出,请求被拒绝。

大致的漏桶限流规则如下:

1、进水口(对应客户端请求)以任意速率流入进入漏桶。

2、漏桶的容量是固定的,出水(放行)速率也是固定的。

3、漏桶容量是不变的,如果处理速度太慢,桶内水量会超出了桶的容量,则后面流入的水滴会溢出,表示请求拒绝。

以一定速率处理请求,有大量流量进入时,会发生溢出,从而限流保护服务可用(也就是削峰),不至于直接请求到服务器,缓冲压力(也就是缓冲)。

优点:一定程度上应对流量突刺,以恒定的速率处理请求,保证服务器安全

缺点:漏桶出口的速度固定,不能灵活的应对后端能力提升。比如,通过动态扩容,后端流量从100QPS提升到1000QPS,漏桶没有办法。

4、令牌桶算法限流(推荐)

管理员提前生成一批令牌,比如每秒 10 个令牌,当用户要操作前,先去拿到一个令牌,有令牌的人就有资格执行操作、能同时执行操作,而拿不到令牌的就拒绝请求得等着。当然了令牌的数量也是有上限的,令牌的数量与时间和发放速率强相关,时间流逝的时间越长,会不断往桶里加入越多的令牌,如果令牌发放的速度比申请速度快,令牌桶会放满令牌,直到令牌占满整个令牌桶。

令牌桶的大小固定,令牌的产生速度固定可以自己设置,但是消耗令牌(即请求)速度不固定(可以应对一些某些时间请求过多的情况);每个请求都会从令牌桶中取出令牌,如果没有令牌则丢弃该次请求。

令牌桶限流大致的规则如下:

1、进水口按照某个速度,向桶中放入令牌。

2、令牌的容量是固定的,但是放行的速度不是固定的,只要桶中还有剩余令牌,一旦请求过来就能申请成功,然后放行。

3、如果令牌的发放速度,慢于请求到来速度,桶内就无牌可领,请求就会被拒绝。

优点:比漏桶更高效,可以方便地应对突发出口流量,能够并发处理同时的请求,并发性能会更高

缺点还是时间单位选取的问题,令牌的产生速度、消耗令牌的速度对应时间选多少合适

限流实现

单机限流

使用谷歌提供的限流工具Guava RateLimiter第三方库实现,对单位时间的请求数量进行限制。

示例代码:

复制代码
import com.google.common.util.concurrent.RateLimiter;

public static void main(String[] args) {
    // 每秒限流10个请求
    RateLimiter limiter = RateLimiter.create(10.0);
    while (true) {
        if (limiter.tryAcquire()) {
            // 处理请求
        } else {
            // 超过流量限制,需要做何处理
        }
    }
}

多机限流

1、使用操作 Redis 的工具库Redisson内置了一个限流工具,分布式限流实现,把用户的使用频率等数据放到一个集中的存储进行统计,这样无论用户的请求落到了哪台服务器,都以集中存储中的数据为准。

2、在网关层集中进行限流和统计,如 Sentinel、Spring Cloud Gateway

Redisson限流实现

参考官方文档:https://github.com/redisson/redisson

首先引入Redisson依赖:

复制代码
<!--        redisson-->
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>4.1.0</version>
</dependency>

还需要有redis的依赖和application.yml配置

复制代码
<!-- redis -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
</dependency>

# Redis 配置
  redis:
    database: 0
    host: localhost
    port: 6379
    timeout: 5000
#    password: 123456

然后写个RedissonConfig配置类:

复制代码
/**
 * Redisson 配置(初始化RedissonClient对象单例)
 */
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {

    private Integer database;

    private String host;

    private Integer port;

    // 如果你的redis默认没有设置密码则不用写
    private String password;

    @Bean
    public RedissonClient getRedissonClient() {
        // 1.创建配置对象
        Config config = new Config();
        config.useSingleServer()
        .setDatabase(database)
        .setAddress("redis://" + host + ":" + port);
        // 2.创建Redisson实例
        return Redisson.create(config);
    }
}

再写个通用的RedissonManager类,便于使用redisson限流功能:

查看源码发现trySetRate方法的图中参数用法在新版的Redisson中是被弃用的,现在推荐使用RateIntervalUnit的替代方案org.redisson.api.RateType和Duration

第一种是:

复制代码
rateLimiter.trySetRate(RateType.OVERALL, 2, Duration.ofSeconds(1));

第二种是:

复制代码
rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.valueOf("SECONDS"));

推荐使用第一种方式,因为:

1、使用 Duration****更符合 Java 8+ 的日期时间 API 设计理念

2、代码更简洁明了

3、这是 Redisson 官方推荐的新用法

因此,最终代码为

复制代码
/**
 * 提供RedissonLimiter限流基础服务
 */
@Service
public class RedissonManager {

    @Resource
    private RedissonClient redissonClient;


    public void doRateLimit(String key){
        // 创建一个限流器
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
        // 设置限流器速率为每秒2个请求
        rateLimiter.trySetRate(RateType.OVERALL, 2, Duration.ofSeconds(1));
        // 每当一个请求过来,尝试获取一个令牌
        boolean canOp = rateLimiter.tryAcquire(1);
        if (!canOp){
            //TOO_MANY_REQUEST_ERROR(42900, "请求过于频繁")
            throw new BusinessException(ErrorCode.TOO_MANY_REQUEST_ERROR, "操作太频繁,请稍后再试");
        }
    }
}

最后在controller中使用它:

复制代码
/**
 * 获取当前登录用户
 *
 * @param request
 * @return
 */
@Override
public User getLoginUser(HttpServletRequest request) {
// 先判断是否已登录
// 用户登录态键String USER_LOGIN_STATE = "user_login";
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User currentUser = (User) userObj;
if (currentUser == null || currentUser.getId() == null) {
    //NOT_LOGIN_ERROR(40100, "未登录")
    throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
}
// 从数据库查询(追求性能的话可以注释,直接走缓存)
long userId = currentUser.getId();
currentUser = this.getById(userId);
if (currentUser == null) {
    throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
}
return currentUser;
}

@Resource
private RedissonManager redissonManager;

User loginUser = userService.getLoginUser(request);
String id = String.valueOf(loginUser.getId());
redissonManager.doRateLimit("generateByAi_" + id);
相关推荐
与遨游于天地2 小时前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209252 小时前
Guava Cache 原理与实战
java·后端·spring
yangminlei2 小时前
Spring 事务探秘:核心机制与应用场景解析
java·spring boot
Yuer20253 小时前
什么是 Rust 语境下的“量化算子”——一个工程对象的最小定义
开发语言·后端·rust·edca os·可控ai
记得开心一点嘛3 小时前
Redis封装类
java·redis
短剑重铸之日3 小时前
《7天学会Redis》Day 5 - Redis Cluster集群架构
数据库·redis·后端·缓存·架构·cluster
lkbhua莱克瓦243 小时前
进阶-存储过程3-存储函数
java·数据库·sql·mysql·数据库优化·视图
计算机程序设计小李同学4 小时前
基于SSM框架的动画制作及分享网站设计
java·前端·后端·学习·ssm
鱼跃鹰飞4 小时前
JMM 三大特性(原子性 / 可见性 / 有序性)面试精简版
java·jvm·面试