架构师必备:限流方案选型(使用篇)

大家好,我是Java烘焙师。为了避免突增流量引起服务雪崩,需要对接口、存储资源做限流保护,根据系统负载情况设置合适的限流值。下面结合笔者的经验和思考,对主要限流方案的选型做一下总结,本篇先看如何使用,下一篇再看背后的原理。

下面介绍几种常见限流方案的使用方法、优缺点:

  • 单机限流:Guava RateLimiter
  • 同时支持单机限流、集群限流:Sentinel
  • 分布式限流:Redisson RateLimiter

1. 单机限流:Guava RateLimiter

使用

官网API文档:https://guava.dev/releases/snapshot-jre/api/docs/com/google/common/util/concurrent/RateLimiter.html

Guava是Google开源的Java类库,提供了很多实用的工具,包括集合、本地缓存、并发编程工具等。Guava RateLimiter就是其中的单机限流工具,核心概念如下:

  • 每秒允许通过的最大permits:
    • 当请求获取1个permit时,就相当于是qps限流
    • 当请求获取多个permits时,代表该请求需要消耗多少资源,比如想限定更新DB的SQL qps为1000,则需获取1000 permits
  • 支持预热:在预热期内逐步支持到最大permits,适用于需要预热缓存资源的场景
  • 允许突发流量:每次请求是否被限流,不取决于该次请求permits的多少,而是取决于前一次请求,前一次请求的permits越多,则后续请求需等待的时间越长
    • 比如有一个空闲的RateLimiter,第一次请求无论是获取1 permit、还是10000 permits,都能立刻成功,但是会计算出下一次permits可用的时间。那么第一次请求是10000 permits时,第二次请求即使只需10 permits也可能要长时间等待

如下分别是阻塞、非阻塞方式,使用Guava RateLimiter的示例。

java 复制代码
    // 创建一个每秒允许10个permits的限流器
    RateLimiter rateLimiter = RateLimiter.create(10.0);

    // 1. 阻塞的方式
    long startTime = System.currentTimeMillis();
    // 获取5 permits;如果充足,则获取成功,否则阻塞等待
    rateLimiter.aquire(5);
    long endTime = System.currentTimeMillis();
    // 打印阻塞等待的时间
    System.out.println("Processing business logic. Waiting time ms: " + (endTime - startTime));

    // 2. 非阻塞的方式
    // 尝试获取1 permit
    if (rateLimiter.tryAcquire()) {
        // 如果能获取到令牌,则继续处理业务逻辑
        System.out.println("Processing business logic...");
    } else {
        // 否则快速失败,返回友好提示或执行降级逻辑
        System.out.println("Too many requests.");
    }

优点

  • 性能极高:纯计算,无网络开销
  • 实现简单

缺点

  • 无法跨实例协同,从整体层面控制限流

2. 同时支持单机限流、集群限流:Sentinel

使用

官网文档:https://sentinelguard.io/zh-cn/docs/introduction.html

Sentinel是阿里开源的流量治理组件,在国内使用较广泛。核心概念如下:

  • 资源:可以是接口(调入、调出均可),也可以是任何一段代码逻辑。可通过sentinel注解、或者sentinel API包装成受保护的资源
  • 规则:围绕资源的实时状态设定的规则,包括流量控制规则、熔断降级规则以及系统保护规则,所有规则都可以动态实时调整。
    • 常见的流量控制规则:
      • 限流阈值类型:支持QPS、线程数模式,默认是按QPS来限流
      • 流控效果:即超过限流阈值时的行为,支持直接拒绝、排队等待、慢启动模式,默认是直接拒绝
    • 常见的熔断降级规则:
      • 熔断策略:即在什么情况下熔断降级,支持慢调用比例、异常比例、异常数策略,默认是慢调用比例
      • 熔断阈值:慢调用比例模式下为慢调用的RT平均响应时间,异常比例/异常数模式下为对应的阈值
  • 集群限流:是为了解决流量在集群内各实例之间不均匀、导致触发单机限流,而达不到总QPS预期的问题。集群限流有2种模式:单机均摊、全局阈值,为了避免token server成为瓶颈,比较典型的是单机均摊。
    • 单机均摊:先设置一个集群限流阈值,然后再根据机器实例数均摊到单机上,即单机限流值=集群限流值/机器实例数
    • 全局阈值:集群流控中共有两种身份
      • Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流
      • Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)

如下分别是通过sentinel注解、sentinel API方式的示例。

  • sentinel注解一般加在受保护的接口、访问存储资源的方法上
  • sentinel API更加灵活,可包住受保护的业务代码片段;更进一步,结合业务入参,可实现热点参数限流
java 复制代码
    // 1. sentinel注解:在sentinel控制台上可针对定义的资源名配置限流规则
    @Service
    @SentinelResource(value = "protectedMethod")
    public String protectedMethod(Request request) {
    }

    // 2. sentinel API
    // 2.1 抛异常方式定义资源
    try (Entry entry = SphU.entry("protectedResource1")) {
        // 被保护的逻辑
        System.out.println("protected resource 1");
    } catch (BlockException ex) {
        // 处理被流控的逻辑
        System.out.println("blocked!");
    }

    // 2.2 返回布尔值方式定义资源
    if (SphO.entry("protectedResource2")) {
        // 务必保证finally会被执行
        try {
            // 被保护的业务逻辑
        } finally {
            SphO.exit();
        }
    } else {
        // 资源访问阻止,被限流或被降级
        // 进行相应的处理操作
    }

    // 2.3 热点参数限流
    try (Entry entry = SphU.entry("hotspotResource", EntryType.IN, 1, paramA, paramB)) {
        // 易出现热点参数的业务逻辑
    } catch (BlockException ex) {
        // 热点参数限流
    } finally {
        if (entry != null) {
            entry.exit(1, paramA, paramB);
        }
    }

优点

  • 易于与Java框架集成,提高应用开发效率:如Spring Cloud、Dubbo等
  • 功能强大:在控制台页面,可动态配置限流规则、熔断降级规则

缺点

  • sentinel的集群限流,在单机均摊模式下最终会换算为单机限流,当集群限流值较低、机器实例数较多时,计算出的单机限流值可能不准,无法精准控制总的集群限流
    • 例如:有100台机器,按30 qps来限流;则计算出的单机均摊qps不足1、按1处理,这样总的集群限流就变成了100 qps,大于预期的30 qps

3. 分布式限流:Redisson RateLimiter

使用

官网API文档:https://redisson.pro/docs/data-and-services/objects/#ratelimiter

Redisson是一个高性能的Redis客户端框架,提供了基于redis实现的分布式工具,如分布式集合、分布式锁、布隆过滤器等,借助Redisson RateLimiter可以实现分布式限流。核心概念如下:

  • 允许在一段时间间隔内,最多有n个permits通过
  • 通过redis lua脚本实现限流逻辑、以及原子操作

如下是使用Redisson RateLimiter的示例。

java 复制代码
    // 使用Redisson的分布式限流器
    RRateLimiter limiter = redisson.getRateLimiter("myLimiter");

    // 限定每60秒100个permits
    limiter.trySetRate(RateType.OVERALL, 100, 60, RateIntervalUnit.SECONDS);
    // 阻塞式获取5 permits
    limiter.acquire(5);
    // 非阻塞式获取1 permit
    if (limiter.tryAcquire(1)) {
        // 获取到permit,执行业务逻辑
    } else {
        // 被限流
    }

优点

  • 基于redis实现了分布式限流,能较精准地控制低qps场景的限流
  • 与特定框架/限流组件无关

缺点

  • 引入了外部依赖,有网络开销,系统稳定性易受影响

其它分布式限流方案

笔者还了解到有一个redis模块 redis-cell 可做分布式限流。不过有以下问题,不是很建议使用:

  • 需要额外运维部署,成本较高
  • 项目由个人维护,目前已基本停滞

结论

  • Java应用开发,一般情况下,可引入Sentinel做接口、存储资源的限流控制
  • 单机任务,如手动执行或定期执行的task,可引入Guava RateLimiter做简易的限流控制
  • 低qps场景的限流、或不想接入特定框架/限流组件的服务,可引入Redisson RateLimiter来实现较精准的分布式限流控制
相关推荐
朱嘉鼎4 小时前
寄存器编写LED程序
stm32·单片机·架构·keilmdk
emma羊羊9 小时前
【Redis】
数据库·redis·缓存
Layer11 小时前
CommonMark 解析策略与 cmark 工程核心代码解析
架构·markdown·设计
我命由我1234513 小时前
Spring Cloud - Spring Cloud 声明式接口调用(Fiegn 声明式接口调用概述、Fiegn 使用)
java·后端·spring·spring cloud·微服务·架构·java-ee
canonical_entropy13 小时前
领域驱动设计(DDD)中聚合根的最主要职责真的是维护一致性吗?
后端·架构·领域驱动设计
aristo_boyunv14 小时前
Redis底层原理-持久化【详细易懂】
数据库·redis·缓存
羊锦磊15 小时前
[ Redis ] SpringBoot集成使用Redis(补充)
java·数据库·spring boot·redis·spring·缓存·json
行百里er15 小时前
ES8.6.2 集群部署:教你避坑,笑着搞定高可用
后端·elasticsearch·架构