GUAVA 实现限流
目录
前言
1. 服务限流算法
1.1 漏桶算法
1.2 令牌桶算法
2. Guava限流
2.1 RateLimiter类
2.2 RateLimiter类-常用方法
2.3 Guava RateLimiter 优缺点对比
3. Guava 示例代码
3.1 基础阻塞式获取令牌
3.2 非阻塞式获取令牌(推荐用于Web场景)
3.3 带预热期的平滑限流
4、 面试回答总结
前言
刚毕业的时候,遇到了一次oom,领导安排给我分析排查解决。我最后实现的方式就是redis+guava。
现在开发中很少用guava限流了,原因是:
单机限流,非分布式,这是最致命的缺点。 Guava的限流作用范围仅限于当前JVM实例。如果你有10台服务器,每台限流100 QPS,那么全局的总限流效果是 100 * 10 = 1000 QPS,而不是你想要的整个集群100 QPS。要实现集群限流,必须引入Redis等分布式协调器,但这就失去了Guava轻量级的优势。但是其限流思想还是很有学习意义的。
1. 服务限流算法
1.1 漏桶算法
漏桶算法的思想:
是将请求放入一个有固定容量的桶中,然后以恒定的速率从桶中取出请求进行处理。如果桶满了,就丢弃新来的请求。这样可以保证请求的处理速率不超过设定的阈值,但是不能应对突发流量,因为突发流量会导致大量请求被丢弃。
漏桶算法的优点:
1. 控制请求的传输速率:漏桶算法可以以固定的速率处理请求,保持请求的传输速率是均匀的,从而可以更好地控制流量。
2. 对于下游系统的保护:漏桶算法在流量超过处理能力时,会丢弃请求,防止流量过载对下游系统造成冲击。
漏桶算法的缺点:
1. 无法应对瞬时突发流量:漏桶算法以固定速率处理请求,无法允许瞬时突发的高速访问。
2. 有可能导致请求的延迟增加:当漏桶中没有足够的请求可以处理时,请求可能会排队等待,导致延迟增加。
1.2 令牌桶算法
令牌桶算法的思想:
系统以恒定的速率向一个有固定容量的桶中放入令牌,然后每个请求都需要从桶中取出一个令牌才能被处理。如果桶中没有令牌,就暂时等待或者丢弃请求。这样可以保证请求的平均处理速率不超过设定的阈值,同时也允许一定程度的突发流量,因为当桶中有足够的令牌时,可以一次性处理多个请求。
令牌桶算法的优点:
1. 瞬时突发:令牌桶算法允许请求以瞬间的高速率通过,只要令牌桶中有足够的令牌。
2. 平滑流量:令牌桶算法通过固定速率往令牌桶中添加令牌,可以平滑流量的突发。
3. 灵活性:令牌桶算法可以根据具体需求动态调整令牌的注入速率。
令牌桶算法的缺点:
1. 对于处理速率的限制可能不够严格:当令牌产生速率很高时,可能会出现在短时间内处理过多请求的情况,这可能导致系统的负载增加。
2. Guava限流
2.1 RateLimiter类
RateLimiter的核心思想是,它会以一定的速率生成令牌,然后在调用时根据令牌的可用性来控制操作的执行。如果没有令牌可用,RateLimiter可以通过阻塞线程来限制操作的执行速率,或者返回一个失败的结果。
-
令牌添加:系统以固定的时间间隔(如
1/QPS
秒)向桶中添加一个令牌。 -
令牌获取:
-
有令牌:直接获取,请求立即通过。
-
无令牌:
-
可以等待:计算需要等待多久才能有下一个令牌,线程阻塞直到有可用令牌(平滑突发限流模式)。
-
无法等待:直接返回获取失败(非阻塞模式)。
-
-
2.2 RateLimiter类-常用方法
2.3 Guava RateLimiter 优缺点对比
特性维度 | 优点 (Advantages) | 缺点 (Disadvantages / Limitations) |
---|---|---|
部署与依赖 | 极其轻量级,零外部依赖。仅是一个Jar包中的工具类,开箱即用,无需搭建任何额外的服务器或中间件。 | 仅限于单机限流。无法进行集群级别的分布式限流,多台实例无法共享流量状态。这是其最致命的缺点。 |
性能 | 基于内存,性能极高。所有计算在单JVM内完成,无网络开销,性能损耗极低(纳秒级别)。 | 同步阻塞可能带来风险。acquire() 方法会阻塞线程,在Web容器中可能占满工作线程,影响整体吞吐量。 |
算法能力 | 基于令牌桶算法,支持突发流量。允许消耗未来时间段的令牌,应对合理的瞬时流量高峰。 | 功能单一,缺乏治理生态。只是一个“限流器”,而非“治理平台”。 |
高级特性 | 提供平滑预热模式。系统启动时速率从低平滑过渡到设定值,保护冷系统,防止被流量击垮。 | 无熔断降级能力。无法在服务调用失败率过高时自动熔断并执行降级逻辑。 |
易用性 | API 简单直观。create() , acquire() , tryAcquire() 等方法非常容易理解和使用。 | 无系统自适应保护。无法根据系统的CPU负载、并发线程数等实时指标动态调整限流阈值。 |
监控与配置 | - | 规则配置硬编码。限流规则写在代码中,修改必须发布应用,无法实现动态配置、实时生效。 |
适用场景 | - | 无实时监控与控制台。缺乏对QPS、拒绝数量等 metrics 的可视化监控和集中管理界面。 |
- | 不支持热点参数限流。无法对同一接口的不同参数(如不同商品ID)进行精细化区别限流。 |
3. Guava 示例代码
3.1 基础阻塞式获取令牌
import com.google.common.util.concurrent.RateLimiter;public class GuavaRateLimiterDemo {// 创建一个速率限制器,限制为每秒处理2个请求private static final RateLimiter rateLimiter = RateLimiter.create(2.0);public static void main(String[] args) {// 模拟10个请求for (int i = 0; i < 10; i++) {// 请求许可证(令牌),如果无法立即获得,则会阻塞直到可用double waitTime = rateLimiter.acquire();System.out.println("处理请求 " + i + ",等待了 " + waitTime + " 秒");handleRequest(i);}}private static void handleRequest(int index) {System.out.println(System.currentTimeMillis() + " -> 请求 " + index + " 正在执行...");}
}
输出分析:
你会发现前两个请求几乎无需等待,因为桶中有令牌。从第三个请求开始,每个请求大约需要等待0.5秒(1秒/2个),因为速率被限制在了2 QPS。输出会显示等待时间。
处理请求 0,等待了 0.0 秒
1651234567890 -> 请求 0 正在执行...
处理请求 1,等待了 0.0 秒
1651234567891 -> 请求 1 正在执行...
处理请求 2,等待了 0.499 秒 // 开始等待
1651234568390 -> 请求 2 正在执行...
...
3.2 非阻塞式获取令牌(推荐用于Web场景)
这种方式更友好,不会阻塞业务线程。
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;@RestController
public class DemoController {// 限流器:每秒只允许处理10个请求private static final RateLimiter rateLimiter = RateLimiter.create(10.0);@GetMapping("/test")public String testEndpoint(HttpServletResponse response) throws IOException {// 尝试获取令牌,设置超时时间if (rateLimiter.tryAcquire()) { // 成功获取令牌,执行正常业务逻辑return "Success! Time: " + LocalDateTime.now();} else {// 获取失败,立即返回错误信息,避免阻塞response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); // 429response.getWriter().write("Too many requests, please try again later.");return null;}}
}
3.3 带预热期的平滑限流
public class WarmUpDemo {// 创建一个限流器:最终速率是每秒5个请求,预热期为10秒// 在这10秒内,速率会从低平滑地增加到5private static final RateLimiter rateLimiter = RateLimiter.create(5.0, 10, TimeUnit.SECONDS);public static void main(String[] args) {while (true) {double waitTime = rateLimiter.acquire();System.out.println("获取到令牌,等待时间: " + waitTime);// 在预热初期,waitTime会较长,随后逐渐变短并稳定。}}
}
4、 面试回答总结
面试官:“你怎么看Guava的RateLimiter?会在什么场景下使用它?”
你的回答:
“Guava RateLimiter是一个基于令牌桶实现的、非常轻量高效的单机限流客户端库。它的优点是零依赖、性能极高、支持突发流量和预热模式。
但是,它的局限性也非常明显:
第一,它是单机限流,无法做集群级别的流量控制。
第二,功能比较单一,缺乏熔断降级、动态规则、系统自适应保护等高级治理功能。
因此,在我们的技术选型中:
-
对于简单的单体应用、测试环境、或者需要快速实现一个轻量级限流功能的场景,Guava是首选,因为它足够简单高效。
-
但对于大规模的分布式微服务系统,我们一定会选择Sentinel这样的全功能治理平台。它能提供分布式集群流控、熔断降级、热点防护、实时监控和控制台动态配置等一系列能力,这才是保障复杂系统高可用的完整解决方案。
Guava像是一把出色的战术匕首,轻便锋利,适合小规模战斗或应急;而Sentinel则是一套完整的装甲武器系统,是为大规模战役准备的。”