Java面试之限流的实现方式小结
作者:Java面试真题解析
限流是指在各种应用场景中,通过技术和策略手段对数据流量、请求频率或资源消耗进行有计划的限制,以避免系统负载过高、性能下降甚至崩溃的情况发生。限流的目标在于维护系统的稳定性和可用性,并确保服务质量。
使用限流的好处有以下几个:
- 保护系统稳定性:过多的并发请求可能导致服务器内存耗尽、CPU 使用率饱和,从而引发系统响应慢、无法正常服务的问题。
- 防止资源滥用:确保有限的服务资源被合理公平地分配给所有用户,防止个别用户或恶意程序过度消耗资源。
- 优化用户体验:对于网站和应用程序而言,如果任由高并发导致响应速度变慢,会影响所有用户的正常使用体验。
- 保障安全:在网络层面,限流有助于防范 DoS/DDoS 攻击,降低系统遭受恶意攻击的风险。
- 运维成本控制:合理的限流措施可以帮助企业减少不必要的硬件投入,节省运营成本。
在 Java 中,限流的实现方式有很多种,例如以下这些:
- 单机限流:使用 JUC 下的 Semaphore 限流,或一些常用的框架,例如 Google 的 Guava 框架进行限流,但这种限流方式都是基于 JVM 层面的内存级别的单台机器限流。
- 网关层限流:单机限流往往不适用于分布式系统,而分布式系统可以在网关层限流,如 Spring Cloud Gateway 通过 Sentinel、Hystrix 对整个集群进行限流。
- Nginx 限流:通常在网关层的上游,我们会使用 Nginx 一起来配合使用,也就是用户请求会先到 Nginx(或 Nginx 集群),然后再将请求转发给网关,网关再调用其他的微服务,从而实现整个流程的请求调用,因此 Nginx 限流也是分布式系统中常用的限流手段。
它们限流的具体实现如下。
1.单机限流
JVM 层面多线程级别的限流可以使用 JUC 下的 Semaphore,具体使用示例如下:
import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class SemaphoreExample { private final Semaphore semaphore = new Semaphore(5); // 只允许5个线程同时访问 public void accessResource() { try { semaphore.acquire(); // 获取许可,如果当前许可数不足,则会阻塞 System.out.println(Thread.currentThread().getName() + "获得了许可,正在访问资源..."); // 模拟访问资源的时间消耗 Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "访问资源结束,释放许可..."); } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(); } finally { semaphore.release(); // 访问结束后释放许可 } } public static void main(String[] args) { SemaphoreExample example = new SemaphoreExample(); for (int i = 0; i < 10; i++) { new Thread(() -> example.accessResource()).start(); } } }
想要实现更平滑的单机限流,可以考虑 Google 提供的 Guava 框架,它的使用示例如下。
首先在 pom.xml 添加 guava 引用,配置如下:
<!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.2-jre</version> </dependency>
具体实现代码如下:
import com.google.common.util.concurrent.RateLimiter; import java.time.Instant; /** * Guava 实现限流 */ public class RateLimiterExample { public static void main(String[] args) { // 每秒产生 10 个令牌(每 100 ms 产生一个) RateLimiter rt = RateLimiter.create(10); for (int i = 0; i < 11; i++) { new Thread(() -> { // 获取 1 个令牌,获取到令牌就执行,否则就阻塞等待 rt.acquire(); System.out.println("正常执行方法,ts:" + Instant.now()); }).start(); } } }
2.网关层限流
在 Spring Cloud Gateway 网关层限流,可以借助 Sentinel 等限流框架来实现,它的实现步骤如下。
首先,在 pom.xml 中添加 Gateway 和 Sentinel 相关依赖,如下所示:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> </dependency>
配置限流相关的规则,如下示例所示:
spring: application: name: gate-way-blog cloud: sentinel: transport: dashboard: localhost:18080 scg: # 配置限流之后,响应内容 fallback: # 两种模式,一种是 response 返回文字提示信息, # 另一种是 redirect 重定向跳转,不过配置 redirect 也要配置对应的跳转的 uri mode: response # 响应的状态 response-status: 200 # 响应体 response-body: '{"code": -10,"message": "被熔断或限流!"}'
最后在 Sentinel 控制台配置网关的限流设置即可,当然也可以使用 Nacos 作为数据源,两者选择配置其中一个即可。
3.Nginx 限流
Nginx 提供了两种限流手段:
- 通过控制速率来实现限流。
- 通过控制并发连接数来实现限流。
我们一个一个来看。
3.1 控制速率实现限流
我们需要使用 limit_req_zone 用来限制单位时间内的请求数,即速率限制,示例配置如下:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit; } }
以上配置表示,限制每个 IP 访问的速度为 2r/s,因为 Nginx 的限流统计是基于毫秒的,我们设置的速度是 2r/s,转换一下就是 500ms 内单个 IP 只允许通过 1 个请求,从 501ms 开始才允许通过第 2 个请求。
我们使用单 IP 在 10ms 内发并发送了 6 个请求的执行结果如下:
从以上结果可以看出他的执行符合我们的预期,只有 1 个执行成功了,其他的 5 个被拒绝了(第 2 个在 501ms 才会被正常执行)。
速率限制升级版
上面的速率控制虽然很精准但是应用于真实环境未免太苛刻了,真实情况下我们应该控制一个 IP 单位总时间内的总访问次数,而不是像上面那么精确但毫秒,我们可以使用 burst 关键字开启此设置,示例配置如下:
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit burst=4; } }
burst=4 表示每个 IP 最多允许4个突发请求,如果单个 IP 在 10ms 内发送 6 次请求的结果如下:
从以上结果可以看出,有 1 个请求被立即处理了,4 个请求被放到 burst 队列里排队执行了,另外 1 个请求被拒绝了。
3.2 控制并发数实现限流
利用 limit_conn_zone 和 limit_conn 两个指令即可控制并发数,示例配置如下:
limit_conn_zone $binary_remote_addr zone=perip:10m; limit_conn_zone $server_name zone=perserver:10m; server { ... limit_conn perip 10; limit_conn perserver 100; }
其中 limit_conn perip 10 表示限制单个 IP 同时最多能持有 10 个连接;limit_conn perserver 100 表示 server 同时能处理并发连接的总数为 100 个。
小贴士:只有当 request header 被后端处理后,这个连接才进行计数。
课后思考
Semaphore 限流和 Guava 限流有什么区别?Sentinel 和 Nginx 限流有什么不足?应该如何避免?
到此这篇关于Java面试之限流的实现方式小结的文章就介绍到这了,更多相关Java限流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!