一文打尽限流规则

169 阅读7分钟

一说起互联网,大家总是会想起什么高并发、高可用了、开猿节流🤣、降本增笑😎 好像跑偏了赶紧拉回来。大家都不可避免的要处理限流这一问题,可能这么说大家不太理解,为什么要处理限流?难道我们大家同时访问一个网站或者是访问同一个商品详情还不行了嘛?你系统做的烂关我们访问什么事🤷‍♂️.....,其实这事还真的不能完全赖我们,想想每当双十一时那访问量如果投放到一些小的网站上会直接被打崩,所以说当老板们让我们限流时我们的方案就是带加钱😆

好了,现在进入正题——为何需要引入限流机制。简而言之,当用户访问量过大(即流量激增),系统可能面临处理不过来的问题,进而导致服务中断。引入限流有三个主要目的:

  1. 保护系统稳定运行:通过限制流量,可以有效避免因访问量过大而导致系统崩溃。
  2. 提升用户体验:想象一下,如果流量过大使得系统响应缓慢甚至接近崩溃,那么正在使用系统的您会明显感受到延迟或查询无果的情况(这里不考虑由系统本身故障引发的问题)。限流能够确保服务的顺畅运行,减少此类问题的发生。
  3. 防范恶意行为:此外,限流还能有效阻止某些恶意操作,例如频繁地调用我们的API接口,尤其是短信验证接口等。在这种情况下,我们可以设置在特定时间内只允许调用一定次数,并对异常行为进行监控,必要时封锁相关IP地址。

本文主要介绍三种常见的限流方式,前两种侵入代码比较多,但是没有过多限制。后面一种则是比较适合alibabacloud项目,通过配置来达到限流。

前言

在了解上述三种常见的限流方式之外,让我们先了解限流的几种算法(本文由于篇幅有限,不过多解释)

  • 计数器(固定窗口)
  • 滑动窗口
  • 漏桶
  • 令牌桶

如果大家对于上述算法思想不太了解则可以先看限流算法总结:计数器、滑动窗口、漏桶算法、令牌桶算法_滑动窗口 token-CSDN博客这篇文章了解一下,然后再来继续阅读后续内容。

Guava

是Google开源的工具包,其中不仅仅包含限流等工具,感兴趣的小伙伴们可以自行了解哦。

引入Guava依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

让我们来看看简单使用吧

@Configuration
public class RateLimiterConfig {
    @Bean
    public RateLimiter rateLimiter(){
        // 每秒QPS,每秒创建5个令牌(速率),预热1S,慢慢(速率)到达最大值.也可以不设置预热.
        return RateLimiter.create(5,1, TimeUnit.SECONDS);
    }
}

大家也可以不采用这样的方式,后续会介绍

@RestController
@RequestMapping("/rateLimiter")
public class RateLimiterController {
    @Autowired
    private RateLimiter rateLimiter;
    
    @GetMapping("/test")
    public String testRateLimiter() {
        if (rateLimiter.tryAcquire()) {
            return "测试成功";
        } else {
            return "手速太快了";
        }
    }
}

此时启动项目,并在浏览器输入网址,使出单身20年的手速快速刷新看看会有什么效果吧,如果没有效果则可以调低生产令牌的速率

如果有的小伙伴不喜欢上述的定义方式(bean)注入,则可以直接在Controller中使用变量的形式,比如

image.png

当前看来,似乎一切就绪,实际上也确实如此。然而,如果此时有多个接口都需要进行流量限制,那么是否意味着我们需要为每个接口都添加类似的代码呢?虽然实现起来并不复杂,但这种方式略显重复。有没有更好的解决方案呢?这时,我们可以考虑采用AOP(面向切面编程)的方式来抽取这部分业务逻辑。感兴趣的朋友可以先尝试自己动手实现一下。本文暂不展开这一方法的具体内容,后续文章中我会提供详细说明,并附上链接,敬请关注!

redisson限流

对于初次接触这个工具包的朋友们来说,可能一开始会觉得有些陌生。但如果对 Redis 有所了解的话,就会发现两者之间有不少相通之处。简单来说,这个工具包就像是一个集成了多种 Redis 功能的工具箱,其中就包括了非常实用的分布式锁功能。由于它内置了许多与 Redis 相关的工具,所以在使用前需要进行相应的 Redis 配置。

spring:
  redis:
    database: 1
    host: localhost
    port: 6379
    timeout: 5000
    password: 123456

java配置类

@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {
    private String host;
    private String password;
    private String port;
    private int database;
    
    @Bean
    public RedissonClient redissonClient(){
        String redisAddress = String.format("redis://%s:%s",host,port);
        // 1. Create config object
        Config config = new Config();
        // useClusterServers 集群配置
        config.useSingleServer()  // 单节点
                .setAddress(redisAddress)
//                建议一般与业务代码区分,使用不同的db
                .setDatabase(database)
                .setPassword(password);
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
    
    
    @Bean
    public RRateLimiter getRateLimiter(RedissonClient redissonClient){
        // 创建一个限流器
        RRateLimiter rRateLimiter = redissonClient.getRateLimiter("test");
        // 设置速率,第一个参数表示类型,OVERALL表示的是所有客户端共享该限流规则,PER_CLIENT则表示每个客户端独享
        rRateLimiter.setRate(RateType.OVERALL,4L,1, RateIntervalUnit.SECONDS);
        return rRateLimiter;
    }
}

简单使用

@RestController
@RequestMapping("/test")
public class RedissonRateLimiter {

    @Autowired
    private RRateLimiter rateLimiter;

    @GetMapping("redisson_test")
    public String redissonTest() {
        boolean acquire = rateLimiter.tryAcquire();
        if (acquire){
            return "redisson_test";
        }else {
            return "手速太快";
        }
    }
}

如果小伙伴们将上述配置类型更改为 PER_CLIENT,并在本地同时打开两个不同的浏览器进行测试,会发现一个有趣的现象:当第一个浏览器触发限流后,第二个浏览器尝试访问时同样受到了限流的影响。这似乎与我们的预期不符。原因在于我们使用了 @Bean 注解来创建限流器,这意味着在同一节点中,限流器是以单例模式存在的。因此,在这种情况下,所有使用该限流器的服务实例都将遵循相同的限流规则。

如果有小伙伴们说我就是想要每个客户端都能独享那该怎么办呢?这个也简单,那就确保每个客户端都能拿到属于自己的限流器即可。比如:

@GetMapping("redisson_test2")
public String redissonTest(HttpServletRequest request) {
    String clientId = request.getSession().getId(); // 或者使用其他唯一标识符
    RRateLimiter rRateLimiter = redissonClient.getRateLimiter("client:" + clientId);
    // 检查限流器是否已存在,如果不存在则设置限流规则
    rRateLimiter.trySetRate(RateType.PER_CLIENT, 3L, 5, RateIntervalUnit.SECONDS);
    boolean acquire = rRateLimiter.tryAcquire();
    if (acquire){
        return "redisson_test";
    } else {
        return "手速太快";
    }
}

sentinel限流

Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。

首先引入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

还需要引入spring-cloud-alibaba 这里给出了关键依赖

<properties>
    <spring.boot.version>2.2.2.RELEASE</spring.boot.version>
    <spring.cloud.version>Hoxton.SR1</spring.cloud.version>
    <spring.cloud.alibaba>2.1.0.RELEASE</spring.cloud.alibaba>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring.boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring.cloud.alibaba}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

简易properties 配置

spring.cloud.sentinel.transport.dashboard=localhost:8080

此时我们下载sentinel

sentinel下载

如何运行呢?

2024-07-25 16 08 50.png

点击该jar包,然后右击,按照图片指示,然后在控制台输入 java '-Dserver.port=8080' '-Dcsp.sentinel.dashboard.server=localhost:8080' '-Dproject.name=sentinel-dashboard' '-jar' sentinel-dashboard-1.8.6.jar 如果你没有选择下载跟我相同的版本的jar包,则自行更改命令中的版本号。

此时输入Sentinel Dashboard 就会呈现出 账号和密码默认都是 sentinel

image.png

刚开始是什么都没有的

image.png

此时我们在启动我们的项目之前写一个测试Controller,测试代码如下:

@RestController
@RequestMapping("/sentinel")
public class SentinelController {

    @GetMapping("/test")
    public String testSentinelLimiter() {
        return "testSentinelLimiter";
    }
}

然后启动之后,访问该接口(此时访问上述的测试用例也是可以的),然后再次打开我们的sentinel控制面板,会发现不一样了。

image.png

然后点进去设置一些流控规则

image.png

image.png

表示的是每s钟可以处理3个请求

当我们大力访问该url时,会出现如下异常提示

image.png

此时我们可以使用SentinelResource来标记需要限流的资源,比如如下

@GetMapping("/test")
@SentinelResource(value = "testSentinelLimiter",blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
public String testSentinelLimiter() {
    return "testSentinelLimiter";
}
public class ExceptionUtil {
    public static String handleException(BlockException ex) {
        return "操作太快了";
    }
}

此时访问该url,再次查看sentinel控制台

image.png

可以设置流控规则(删除上述配置的流控规则,避免出现影响)

然后在大力访问该url看看效果吧。

更多详细操作大家可以参考点我一下

总结

至此,本篇文章对三种限流方法进行了初步的介绍。后续内容中,我将逐步补充更多详细的解析。如有不当之处,恳请各位大佬多多指教。