“喂,小王啊,最近系统访问量激增,经常卡顿,你看看能不能想想办法?”
“收到,领导!我这就去排查!”
作为一名 Java 开发者,相信你对上面这段对话一定不陌生。在高并发场景下,系统很容易因为流量过大而出现各种问题,比如响应变慢、CPU 飙升、甚至直接崩溃。为了避免这些问题,限流就成了我们必须掌握的一项技能。
今天,咱们就来聊聊 Java 并发包 java.util.concurrent
中的一个限流利器——Semaphore
。
什么是 Semaphore?
Semaphore
,中文名“信号量”,是一种基于计数的同步工具。你可以把它想象成一个“许可证”管理器。它维护了一组许可证,线程在执行操作前需要先获取许可证,执行完毕后再释放许可证。如果许可证被拿光了,其他线程就只能等着。
Semaphore
有两种主要的构造方法:
Semaphore(int permits)
:创建具有给定数量许可证的Semaphore
,使用非公平的同步策略。Semaphore(int permits, boolean fair)
:创建具有给定数量许可证的Semaphore
,并可以指定是否使用公平的同步策略。fair
为true
时,表示采用公平策略,即等待时间最长的线程优先获取许可证;fair
为false
时,则采用非公平策略,即线程获取许可证的顺序是不确定的。
Semaphore
最常用的两个方法是:
acquire()
:获取一个许可证,如果许可证不可用,则当前线程会被阻塞,直到有许可证被释放。release()
:释放一个许可证,将其返回给Semaphore
。
Semaphore 如何实现限流?
Semaphore
的核心思想就是控制并发线程的数量。通过限制同时访问特定资源的线程数量,我们可以有效地控制系统的负载,防止系统被过多的请求压垮。
举个例子,假设我们有一个接口,最多只允许 10 个线程同时访问。我们可以这样使用 Semaphore
:
import java.util.concurrent.Semaphore;
public class RateLimiter {
private static final Semaphore semaphore = new Semaphore(10); // 允许10个线程同时访问
public void accessResource() {
try {
semaphore.acquire(); // 获取许可证
// 模拟业务逻辑处理
System.out.println(Thread.currentThread().getName() + " 正在访问资源...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可证
System.out.println(Thread.currentThread().getName() + " 访问结束,释放许可证");
}
}
public static void main(String[] args) {
RateLimiter rateLimiter = new RateLimiter();
// 创建20个线程来模拟并发请求
for (int i = 0; i < 20; i++) {
Thread thread = new Thread(() -> {
rateLimiter.accessResource();
});
thread.setName("Thread-" + i);
thread.start();
}
}
}
在这个例子中,我们创建了一个 Semaphore
,并设置了 10 个许可证。每个线程在访问 accessResource()
方法前都需要先调用 semaphore.acquire()
获取许可证。如果许可证不足,线程就会被阻塞,直到有其他线程释放许可证。在 finally
块中,我们调用 semaphore.release()
释放许可证,以便其他线程可以继续访问。
运行这段代码,你会发现,最多只有 10 个线程能够同时打印“正在访问资源...”,其他线程则需要等待。
如何根据业务需求设置限流策略?
上面的例子只是一个简单的演示。在实际应用中,我们需要根据业务需求来设置更精细的限流策略。
确定限流的粒度:
- 接口级别: 对整个接口进行限流,这是最常见的限流方式。
- 方法级别: 对某个具体的方法进行限流,适用于更细粒度的控制。
- 用户级别: 对单个用户进行限流,防止某个用户占用过多资源。
- IP 级别: 对单个 IP 地址进行限流,防止恶意攻击。
确定限流的指标:
- QPS (Queries Per Second): 每秒请求数,这是最常用的限流指标。
- TPS (Transactions Per Second): 每秒事务数,适用于事务性操作的限流。
- 并发数: 同时处理的请求数,适用于控制系统的并发压力。
确定许可证的数量:
许可证的数量需要根据系统的实际承载能力来确定。可以通过压力测试来模拟不同的并发量,观察系统的性能指标,如 CPU 使用率、内存使用率、响应时间等,找到一个合适的阈值。
一般来说,许可证的数量可以设置为系统能够处理的最大并发数。如果许可证数量设置过小,会导致系统资源利用率不足;如果许可证数量设置过大,则起不到限流的作用。
选择公平性策略:
在大多数情况下,非公平策略的性能会更好。因为公平策略需要维护一个等待队列,这会增加额外的开销。但是,如果你的业务场景对公平性有要求,比如需要保证先来的请求先被处理,那么就应该选择公平策略。
Semaphore 与其他限流技术的结合
Semaphore
通常不会单独使用,而是会与其他限流技术结合起来,实现更高级的限流功能。
与 Guava RateLimiter 结合:
Guava RateLimiter
是 Google Guava 库提供的一个基于令牌桶算法的限流工具。它可以实现平滑的限流效果,避免突发流量对系统造成冲击。我们可以将
Semaphore
与RateLimiter
结合起来使用,实现双重限流。Semaphore
用于控制并发数,RateLimiter
用于控制请求速率。// 假设 RateLimiter 限制每秒100个请求,Semaphore限制并发数为50 RateLimiter rateLimiter = RateLimiter.create(100); Semaphore semaphore = new Semaphore(50); public void accessResource() { if (rateLimiter.tryAcquire()) { // 先尝试获取令牌 try { semaphore.acquire(); // 再获取许可证 // ... 执行业务逻辑 ... } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); // 释放许可证 } } else { // 请求被限流 System.out.println("请求过于频繁,请稍后再试"); } }
与 Hystrix 结合:
Hystrix
是 Netflix 开源的一个用于处理分布式系统延迟和容错的库。它提供了熔断、降级、限流等功能。Hystrix
内部也使用了Semaphore
来实现限流。我们可以直接使用Hystrix
的限流功能,而不需要自己手动创建Semaphore
。@HystrixCommand(fallbackMethod = "fallback", commandProperties = { @HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE"), @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10") }) public String doSomething() { // ... 执行业务逻辑 ... } public String fallback() { return "服务繁忙,请稍后再试"; }
与 Sentinel结合:
Sentinel 是阿里巴巴开源的一款流量控制、熔断降级组件。与Hystrix相比,Sentinel 提供了更全面的限流、熔断、降级、系统负载保护等功能,并且支持可视化配置和动态规则更新。Sentinel 同样可以使用信号量进行限流,但更多的是通过配置规则来实现。 你可以在Sentinel的控制台中配置QPS阈值、线程数阈值等,而无需手动编写代码。
注意事项
在使用 Semaphore
时,需要注意以下几点:
避免死锁: 如果多个线程之间存在循环依赖关系,并且都使用了
Semaphore
,就可能会发生死锁。例如,线程 A 持有许可证 1,等待许可证 2;线程 B 持有许可证 2,等待许可证 1。这种情况下,两个线程都会无限期地等待下去,导致死锁。为了避免死锁,需要仔细设计线程之间的协作关系,避免循环依赖。可以使用
tryAcquire()
方法来尝试获取许可证,如果获取失败,则立即释放已经持有的许可证,避免长时间占用资源。及时释放许可证: 无论程序是否发生异常,都应该在
finally
块中释放许可证,确保许可证能够被正确地回收。否则,可能会导致许可证泄漏,最终耗尽所有许可证,导致系统无法正常工作。合理设置超时时间: 在调用
acquire()
方法时,可以设置一个超时时间。如果线程在指定时间内没有获取到许可证,就会放弃等待,避免长时间阻塞。可以使用acquire(long timeout, TimeUnit unit)
方法来设置超时时间。
总结
“领导,我已经用 Semaphore 对系统进行了限流改造,现在系统稳定多了!”
“干得不错,小王!晚上请你吃饭!”
Semaphore
是 Java 并发编程中一个非常实用的工具,可以帮助我们轻松实现限流功能,保护系统免受高并发流量的冲击。掌握 Semaphore
的使用方法,是每个 Java 开发者进阶的必备技能。希望通过本文的介绍,你能对 Semaphore
有一个更深入的理解,并在实际开发中灵活运用。
记住,限流只是系统保护的一种手段,我们还需要结合其他的技术,如熔断、降级、负载均衡等,才能构建一个真正稳定、可靠的高并发系统。
如果你觉得这篇文章对你有帮助,请点赞、收藏、转发,让更多的朋友看到!