Guava:并发控制与限流

欢迎你来读这篇博客,这篇博客主要是关于Guava 并发控制与限流
其中包括 Monitor、Guard、enterWhen、enterIf、leave,与 synchronized、Lock、Condition 的对比,以及 RateLimiter 的基本概念、漏桶视角、令牌桶算法、SmoothBursty、SmoothWarmingUp、预热限流、突发流量处理、接口限流和任务提交限流实战。

序言

并发控制和限流,是 Java 后端开发绕不开的两个主题。

并发控制解决的是:

1
多个线程同时访问共享资源时,如何保证安全和有序。

限流解决的是:

1
请求太多、任务太多、调用太快时,如何保护系统不被打爆。

比如:

  • 一个队列满了,生产者应该等待;
  • 一个队列空了,消费者应该等待;
  • 一个接口最多每秒处理 100 个请求;
  • 一个任务提交器不能无限制提交任务;
  • 调用第三方 API 每秒不能超过 10 次;
  • 批处理任务不能瞬间把数据库打满;
  • 秒杀接口不能让所有请求同时打到库存服务。

Guava 在并发和限流方面提供了两个很有代表性的工具:

1
2
com.google.common.util.concurrent.Monitor
com.google.common.util.concurrent.RateLimiter

Monitor 用来简化锁和条件等待。

RateLimiter 用来做单机限流。

这一章对应课程结构:

1
2
3
4
第 8 章:并发控制与限流
8.1 Monitor 使用讲解
8.2 RateLimiter 在漏桶限流算法中的使用
8.3 RateLimiter 令牌桶算法的使用

对应原课程:

1
2
3
24 Guava 之 Monitor 使用讲解
25 Guava 之 RateLimiter 在漏桶限流算法中的使用
26 Guava 之 RateLimiter 令牌桶算法的使用

需要先说明一点:

Guava RateLimiter 的实现更接近令牌桶思想,内部通过 storedPermits、stableIntervalMicros 等机制控制许可发放;但从调用者视角看,acquire() 会让请求按固定速率通过,因此也可以用“平滑漏出”的漏桶视角去理解它的限速效果。

所以这一章会同时讲:

  • 漏桶算法的限速思想;
  • 令牌桶算法的突发处理能力;
  • Guava RateLimiter 的 SmoothBursty 和 SmoothWarmingUp。

这一章的重点不是背源码,而是掌握工程判断:

什么时候用锁,什么时候用条件等待,什么时候用限流,什么时候 Guava RateLimiter 不够,需要 Redis、Sentinel、Resilience4j 或网关限流。

正文

chapter 1:并发控制和限流分别解决什么问题

并发控制和限流经常一起出现,但它们解决的问题不一样。

1. 并发控制

并发控制关注的是共享资源安全。

例如:

1
2
3
4
queue.add(item);
queue.remove();
count++;
map.put(key, value);

如果多个线程同时执行,就可能出现:

  • 数据错乱;
  • 丢数据;
  • 重复消费;
  • 条件判断失效;
  • 死锁;
  • 线程永久等待。

常见工具包括:

1
2
3
4
5
6
7
8
synchronized
ReentrantLock
Condition
Semaphore
CountDownLatch
CyclicBarrier
BlockingQueue
Monitor

2. 限流

限流关注的是单位时间内允许多少请求通过。

例如:

1
2
3
每秒最多 100 个请求
每秒最多提交 10 个任务
每分钟最多调用第三方接口 60 次

常见工具包括:

1
2
3
4
5
6
7
8
9
10
RateLimiter
Semaphore
令牌桶
漏桶
滑动窗口
固定窗口
Sentinel
Resilience4j
Redis Lua
网关限流

并发控制是“同时有多少人在操作”。

限流是“单位时间允许多少人进来”。

这两个概念不要混。

chapter 2:Guava Monitor 是什么

Monitor 是 Guava 提供的同步工具。

包名:

1
com.google.common.util.concurrent.Monitor

它可以理解为:

一个更结构化的锁工具,封装了 ReentrantLock 和条件等待逻辑。

Monitor 的核心概念有两个:

1
2
Monitor
Monitor.Guard

Monitor 表示一个锁。

Guard 表示一个进入锁的条件。

典型使用方式:

1
2
3
4
5
6
7
8
private final Monitor monitor = new Monitor();

private final Monitor.Guard notFull = new Monitor.Guard(monitor) {
@Override
public boolean isSatisfied() {
return count < capacity;
}
};

然后:

1
2
3
4
5
6
monitor.enterWhen(notFull);
try {
// 条件满足后执行
} finally {
monitor.leave();
}

这比手写:

1
2
3
4
5
6
7
8
lock.lock();
try {
while (!condition) {
condition.await();
}
} finally {
lock.unlock();
}

更简洁。

chapter 3:Monitor 和 Guard 的关系

Monitor 是锁。

Guard 是条件。

一个 Monitor 可以有多个 Guard。

例如有界队列中:

1
2
notFull:队列未满,生产者可以放入
notEmpty:队列非空,消费者可以取出

代码结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private final Monitor monitor = new Monitor();

private final Monitor.Guard notFull = new Monitor.Guard(monitor) {
@Override
public boolean isSatisfied() {
return queue.size() < capacity;
}
};

private final Monitor.Guard notEmpty = new Monitor.Guard(monitor) {
@Override
public boolean isSatisfied() {
return !queue.isEmpty();
}
};

Guard 的核心方法是:

1
public boolean isSatisfied()

它表示:

1
当前条件是否满足。

只有条件满足,线程才能继续进入关键区。

chapter 4:Monitor 基本使用

最基本的 Monitor 用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import com.google.common.util.concurrent.Monitor;

public class MonitorCounter {

private final Monitor monitor = new Monitor();

private int count = 0;

public void increment() {
monitor.enter();

try {
count++;
} finally {
monitor.leave();
}
}

public int getCount() {
monitor.enter();

try {
return count;
} finally {
monitor.leave();
}
}
}

这里的:

1
2
monitor.enter()
monitor.leave()

类似于:

1
2
lock.lock()
lock.unlock()

必须保证:

1
monitor.leave()

在 finally 中执行。

否则发生异常时锁不会释放。

chapter 5:enterWhen

enterWhen 用于:

等待某个 Guard 条件满足后进入 Monitor。

示例:

1
2
3
4
5
6
monitor.enterWhen(notFull);
try {
// 条件满足后执行
} finally {
monitor.leave();
}

如果条件不满足,当前线程会等待。

条件满足后,它会进入临界区。

enterWhen 是 Monitor 最核心的方法之一。

它对应 Lock + Condition 中的:

1
2
3
while (!condition) {
await();
}

但写法更高层。

chapter 6:enterIf

enterIf 用于:

如果条件立即满足,就进入 Monitor;如果不满足,不等待,直接返回 false。

示例:

1
2
3
4
5
6
7
8
9
if (monitor.enterIf(notFull)) {
try {
// 条件满足,执行逻辑
} finally {
monitor.leave();
}
} else {
// 条件不满足,直接失败
}

它适合非阻塞场景。

例如:

  • 队列满了就返回失败;
  • 系统忙就快速拒绝;
  • 状态不满足就不等待;
  • 尝试获取资源,不强制等待。

enterWhen 的区别:

方法 条件不满足时
enterWhen 等待
enterIf 立即返回 false

chapter 7:leave

leave 用于离开 Monitor。

所有成功进入 Monitor 的代码,都必须最终调用:

1
monitor.leave();

推荐写法:

1
2
3
4
5
6
7
monitor.enter();

try {
// protected code
} finally {
monitor.leave();
}

或者:

1
2
3
4
5
6
7
monitor.enterWhen(guard);

try {
// protected code
} finally {
monitor.leave();
}

不要这样:

1
2
3
monitor.enter();
doSomething();
monitor.leave();

因为 doSomething() 可能抛异常。

一旦异常发生,leave() 不执行,就可能导致锁无法释放。

chapter 8:Monitor 实战:有界队列

下面实现一个有界队列。

需求:

  1. 队列有固定容量;
  2. 队列满时,生产者等待;
  3. 队列空时,消费者等待;
  4. 使用 Monitor 和 Guard 实现。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import com.google.common.util.concurrent.Monitor;

import java.util.ArrayDeque;
import java.util.Queue;

public class BoundedQueue<T> {

private final Monitor monitor = new Monitor();

private final Queue<T> queue = new ArrayDeque<>();

private final int capacity;

private final Monitor.Guard notFull = new Monitor.Guard(monitor) {
@Override
public boolean isSatisfied() {
return queue.size() < capacity;
}
};

private final Monitor.Guard notEmpty = new Monitor.Guard(monitor) {
@Override
public boolean isSatisfied() {
return !queue.isEmpty();
}
};

public BoundedQueue(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("capacity must be greater than 0");
}

this.capacity = capacity;
}

public void put(T item) throws InterruptedException {
if (item == null) {
throw new IllegalArgumentException("item can not be null");
}

monitor.enterWhen(notFull);

try {
queue.add(item);
} finally {
monitor.leave();
}
}

public T take() throws InterruptedException {
monitor.enterWhen(notEmpty);

try {
return queue.remove();
} finally {
monitor.leave();
}
}

public int size() {
monitor.enter();

try {
return queue.size();
} finally {
monitor.leave();
}
}
}

这个代码里有两个 Guard:

1
2
notFull
notEmpty

生产者使用:

1
monitor.enterWhen(notFull)

消费者使用:

1
monitor.enterWhen(notEmpty)

这就实现了条件等待。

chapter 9:测试有界队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class BoundedQueueDemo {

public static void main(String[] args) {
BoundedQueue<Integer> queue = new BoundedQueue<>(3);

Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
queue.put(i);
System.out.println("生产:" + i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
Integer value = queue.take();
System.out.println("消费:" + value);

Thread.sleep(300);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

producer.start();
consumer.start();
}
}

当队列满时,生产者会等待。

当队列空时,消费者会等待。

Monitor 把条件等待逻辑封装得比较清晰。

chapter 10:tryPut:使用 enterIf 快速失败

有时我们不希望队列满时一直等。

而是希望:

1
能放就放,不能放就立即返回 false。

可以使用 enterIf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean tryPut(T item) {
if (item == null) {
throw new IllegalArgumentException("item can not be null");
}

if (monitor.enterIf(notFull)) {
try {
queue.add(item);
return true;
} finally {
monitor.leave();
}
}

return false;
}

这适合任务提交场景。

例如线程池队列满了,直接拒绝任务。

chapter 11:tryTake:使用 enterIf 非阻塞获取

1
2
3
4
5
6
7
8
9
10
11
public T tryTake() {
if (monitor.enterIf(notEmpty)) {
try {
return queue.remove();
} finally {
monitor.leave();
}
}

return null;
}

如果队列为空,直接返回 null。

这种非阻塞写法适合:

  • 轮询消费;
  • 尝试获取任务;
  • 不希望线程阻塞;
  • 快速失败场景。

chapter 12:Monitor 与 synchronized 对比

synchronized 是 Java 内置同步机制。

示例:

1
2
3
public synchronized void increment() {
count++;
}

或者:

1
2
3
synchronized (this) {
count++;
}

对比:

对比项 synchronized Monitor
来源 JDK 原生 Guava
锁释放 自动释放 必须 leave
条件等待 wait/notify Guard
可读性 简单场景好 条件复杂时更清晰
可中断等待 wait 可中断 enterWhen 可中断
公平性配置 不直接支持 Monitor 可构造公平锁
推荐场景 简单同步 条件守卫更复杂的同步

如果只是简单互斥,用 synchronized 足够。

如果有多个条件等待,Monitor 的 Guard 可读性更好。

chapter 13:Monitor 与 Lock / Condition 对比

ReentrantLock + Condition 实现有界队列,大概是:

1
2
3
4
5
private final ReentrantLock lock = new ReentrantLock();

private final Condition notFull = lock.newCondition();

private final Condition notEmpty = lock.newCondition();

生产者:

1
2
3
4
5
6
7
8
9
10
11
12
lock.lock();

try {
while (queue.size() == capacity) {
notFull.await();
}

queue.add(item);
notEmpty.signal();
} finally {
lock.unlock();
}

消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lock.lock();

try {
while (queue.isEmpty()) {
notEmpty.await();
}

T item = queue.remove();
notFull.signal();

return item;
} finally {
lock.unlock();
}

Monitor 写法更高层:

1
monitor.enterWhen(notFull);

Guard 把条件表达出来:

1
return queue.size() < capacity;

对比:

对比项 Lock + Condition Monitor
来源 JDK Guava
灵活性 很强 较高
控制粒度 更细 更抽象
样板代码 较多 较少
可读性 需要理解 await/signal Guard 语义更清楚
推荐程度 JDK 标准,通用 Guava 项目可用,学习价值高

如果团队熟悉 JDK 并发工具,Lock + Condition 更标准。

如果项目已有 Guava,Monitor 可以让复杂条件更清晰。

chapter 14:Monitor 使用注意事项

1. leave 必须在 finally 中

1
2
3
4
5
6
7
monitor.enter();

try {
// logic
} finally {
monitor.leave();
}

2. Guard 条件必须访问受 Monitor 保护的状态

例如:

1
queue.size()

这个状态应该在 Monitor 保护下修改。

3. 不要在临界区执行长耗时操作

临界区里不要做:

  • 远程调用;
  • 长时间 IO;
  • sleep;
  • 大量计算;
  • 阻塞等待其他系统。

否则会降低并发性能。

4. 中断要正确处理

enterWhen 可能抛出 InterruptedException

捕获后应该:

1
Thread.currentThread().interrupt();

5. 简单场景不要过度使用

如果只是简单计数,AtomicInteger 可能更合适。

如果只是阻塞队列,JDK ArrayBlockingQueue 已经很好。

chapter 15:限流是什么

限流是指:

限制单位时间内允许通过的请求数量或任务数量。

例如:

1
2
3
每秒最多处理 100 个请求
每秒最多提交 10 个任务
每分钟最多调用第三方接口 60 次

限流的目的不是“让系统变慢”。

限流的目的是:

防止系统被突发流量打垮,让流量按系统能承受的节奏进入。

常见限流场景:

  • 接口限流;
  • 登录限流;
  • 短信发送限流;
  • 任务提交限流;
  • 第三方 API 调用限流;
  • 批处理削峰;
  • 消息消费限速;
  • 数据库写入限速。

chapter 16:常见限流算法

常见限流算法包括:

1. 固定窗口

例如每秒计数一次。

1
2
第 1 秒允许 100 个
第 2 秒允许 100 个

缺点是窗口边界可能瞬间放过 200 个请求。

2. 滑动窗口

把固定窗口拆成多个小窗口,平滑一些。

3. 漏桶算法

请求进入桶,以固定速率流出。

特点是输出稳定。

4. 令牌桶算法

系统按固定速率生成令牌,请求拿到令牌才能通过。

特点是允许一定突发。

5. 并发数限制

限制同时执行的请求数。

例如 Semaphore。

它不是严格 QPS 限流,而是并发限制。

Guava RateLimiter 主要是单机令牌桶风格的平滑限流器。

chapter 17:漏桶算法思想

漏桶算法可以想象成一个桶。

请求像水一样进入桶。

桶底有一个孔,以固定速度漏水。

1
请求进入 -> 桶 -> 固定速率流出

如果请求来得太快:

  • 桶还没满,请求排队;
  • 桶满了,请求被丢弃或拒绝。

漏桶算法的核心是:

输出速率稳定。

适合保护下游系统。

比如数据库只能稳定处理每秒 100 次写入,那就让请求以每秒 100 的速度流出。

chapter 18:RateLimiter 和漏桶视角

Guava RateLimiter 不完全是传统漏桶实现。

但从调用者视角看:

1
rateLimiter.acquire();

会阻塞当前线程,让请求按固定速率通过。

例如:

1
RateLimiter limiter = RateLimiter.create(2.0);

表示每秒 2 个许可。

也就是平均每 500ms 放行一个请求。

这看起来就像漏桶:

1
请求可以很多,但通过速度被平滑限制。

所以很多课程会从漏桶角度讲 RateLimiter。

更准确地说:

Guava RateLimiter 内部是令牌桶风格的许可模型,但它通过等待时间控制实现平滑输出效果。

chapter 19:RateLimiter 基本使用

包名:

1
com.google.common.util.concurrent.RateLimiter

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.google.common.util.concurrent.RateLimiter;

public class RateLimiterBasicDemo {

public static void main(String[] args) {
RateLimiter rateLimiter = RateLimiter.create(2.0);

for (int i = 1; i <= 10; i++) {
double waitedSeconds = rateLimiter.acquire();

System.out.println("任务 " + i
+ " 获取许可,等待秒数 = "
+ waitedSeconds
+ ",time = "
+ System.currentTimeMillis());
}
}
}

RateLimiter.create(2.0) 表示:

1
每秒发放 2 个许可。

acquire() 会阻塞等待直到拿到许可。

返回值是等待的秒数。

chapter 20:acquire

acquire() 表示获取一个许可。

1
rateLimiter.acquire();

如果当前可以立即获取,就立即返回。

如果当前没有许可,就阻塞等待。

也可以一次获取多个许可:

1
rateLimiter.acquire(5);

这表示获取 5 个许可。

注意:

一次获取多个许可会影响后续请求的等待时间。

RateLimiter 允许预支一定许可。

大请求可能会让后面的请求等待更久。

chapter 21:tryAcquire

tryAcquire() 表示尝试获取许可。

1
boolean success = rateLimiter.tryAcquire();

如果当前能拿到许可,返回 true。

否则立即返回 false。

这适合快速失败场景。

例如接口限流:

1
2
3
if (!rateLimiter.tryAcquire()) {
throw new RuntimeException("too many requests");
}

也可以设置等待时间:

1
boolean success = rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS);

表示最多等待 100ms。

如果 100ms 内拿不到许可,就返回 false。

chapter 22:acquire 和 tryAcquire 的区别

方法 行为 适合场景
acquire() 阻塞直到拿到许可 后台任务、批处理、调用第三方限速
tryAcquire() 立即尝试,失败返回 false 接口快速拒绝
tryAcquire(timeout) 最多等待一段时间 可接受短暂排队的接口

如果是用户请求接口,通常不建议无限阻塞。

更推荐:

1
tryAcquire()

或者:

1
tryAcquire(50, TimeUnit.MILLISECONDS)

如果是后台任务,希望慢慢执行,可以用:

1
acquire()

chapter 23:RateLimiter 实战:任务提交限流

假设我们有一个任务提交器,每秒最多提交 5 个任务。

1
2
3
4
5
6
7
8
9
10
11
12
import com.google.common.util.concurrent.RateLimiter;

public class TaskSubmitLimiter {

private final RateLimiter rateLimiter = RateLimiter.create(5.0);

public void submit(Runnable task) {
rateLimiter.acquire();

task.run();
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TaskSubmitLimiterDemo {

public static void main(String[] args) {
TaskSubmitLimiter limiter = new TaskSubmitLimiter();

for (int i = 1; i <= 20; i++) {
int taskId = i;

limiter.submit(() -> {
System.out.println("执行任务:" + taskId
+ ",time = " + System.currentTimeMillis());
});
}
}
}

这样任务不会瞬间全部执行,而是按固定速率通过。

适合:

  • 批量调用第三方接口;
  • 批量写数据库;
  • 批量发送消息;
  • 批量提交任务。

chapter 24:RateLimiter 实战:接口限流

在普通 Java 方法中,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.google.common.util.concurrent.RateLimiter;

public class ApiRateLimitService {

private final RateLimiter rateLimiter = RateLimiter.create(100.0);

public String handleRequest(String request) {
if (!rateLimiter.tryAcquire()) {
return "请求过多,请稍后再试";
}

return "处理成功:" + request;
}
}

在 Spring Boot 中,可以放到 Controller 或拦截器里。

简单 Controller 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoRateLimitController {

private final RateLimiter rateLimiter = RateLimiter.create(10.0);

@GetMapping("/limited")
public String limited() {
if (!rateLimiter.tryAcquire()) {
return "too many requests";
}

return "ok";
}
}

这个示例只是单机限流。

如果服务部署多个实例,每个实例都有自己的 RateLimiter。

总 QPS 会变成:

1
实例数 * 单机限流值

分布式限流要用 Redis、网关、Sentinel 等方案。

chapter 25:令牌桶算法思想

令牌桶算法可以想象成一个桶。

系统按固定速率往桶里放令牌。

请求来了,需要先拿令牌。

拿到令牌才能通过。

1
2
系统生成令牌 -> 放入桶
请求到来 -> 拿令牌 -> 通过

如果桶里暂时积累了一些令牌,突发请求可以一次性拿走多个令牌。

这就是令牌桶和漏桶的区别。

漏桶

输出稳定,突发会被平滑。

令牌桶

平均速率受限,但允许一定突发。

例如:

1
2
每秒生成 10 个令牌
桶容量 100

如果系统空闲 10 秒,桶里可能积累 100 个令牌。

之后瞬间来 100 个请求,可以被快速放行。

这适合允许短暂突发的系统。

chapter 26:RateLimiter 的令牌桶风格

Guava RateLimiter 内部维护类似许可的概念。

关键思想包括:

  • 按固定速率生成许可;
  • 空闲时可以积累一定许可;
  • 请求到来时消费许可;
  • 如果许可不足,就计算需要等待多久;
  • 支持平滑突发和预热模式。

核心实现类包括:

1
2
3
SmoothRateLimiter
SmoothBursty
SmoothWarmingUp

不用背源码细节,但要理解两种模式:

1
2
SmoothBursty:允许突发
SmoothWarmingUp:预热限流

chapter 27:SmoothBursty

RateLimiter.create(double permitsPerSecond) 默认创建的是 SmoothBursty 风格。

1
RateLimiter limiter = RateLimiter.create(10.0);

它表示:

1
稳定速率每秒 10 个许可,并允许一定突发。

突发能力来自 storedPermits。

当系统空闲时,许可可以积累。

之后突发请求可以更快通过。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.google.common.util.concurrent.RateLimiter;

public class SmoothBurstyDemo {

public static void main(String[] args) throws InterruptedException {
RateLimiter limiter = RateLimiter.create(2.0);

Thread.sleep(2000);

for (int i = 1; i <= 5; i++) {
double waited = limiter.acquire();

System.out.println("request " + i + ", waited = " + waited);
}
}
}

由于前面空闲了一段时间,前几个请求可能等待很短。

这就是 bursty 的效果。

chapter 28:SmoothWarmingUp

SmoothWarmingUp 表示预热限流。

创建方式:

1
RateLimiter limiter = RateLimiter.create(10.0, 5, TimeUnit.SECONDS);

含义:

1
目标速率是每秒 10 个许可,但需要 5 秒预热。

刚开始不会立刻达到最大速率。

而是逐渐升高。

适合那些不能瞬间打满的系统。

例如:

  • 冷启动服务;
  • 缓存未预热;
  • 数据库连接刚恢复;
  • JVM 刚启动;
  • 下游系统需要慢慢升流量。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.google.common.util.concurrent.RateLimiter;

import java.util.concurrent.TimeUnit;

public class SmoothWarmingUpDemo {

public static void main(String[] args) {
RateLimiter limiter = RateLimiter.create(5.0, 3, TimeUnit.SECONDS);

for (int i = 1; i <= 10; i++) {
double waited = limiter.acquire();

System.out.println("request " + i + ", waited = " + waited);
}
}
}

前几个请求等待时间可能更长。

后面逐渐接近稳定速率。

chapter 29:预热限流适合什么场景

预热限流适合:

  • 服务刚启动;
  • 缓存刚重建;
  • 数据库刚恢复;
  • 线程池刚开始工作;
  • 下游系统刚从故障中恢复;
  • JVM JIT 和连接池还没热起来。

它的目的不是单纯限制 QPS。

而是避免系统从 0 瞬间被打到满载。

这对一些需要预热的系统非常重要。

chapter 30:突发流量处理

突发流量有两种处理方式。

1. 完全平滑

所有请求都按固定速度进入。

优点:

  • 下游压力稳定。

缺点:

  • 突发请求排队时间长。

更像漏桶。

2. 允许适度突发

平均速率受限,但允许短时间消耗存量令牌。

优点:

  • 用户体验更好;
  • 空闲后的请求可以快速处理。

缺点:

  • 下游要能承受短暂峰值。

更像令牌桶。

Guava RateLimiter 默认的 SmoothBursty 允许一定突发。

如果下游非常脆弱,不希望突发,可以设置更保守的速率,或引入队列和并发限制。

chapter 31:RateLimiter 和 Semaphore 的区别

很多人会把 RateLimiter 和 Semaphore 混淆。

对比项 RateLimiter Semaphore
控制目标 单位时间速率 同时并发数量
典型含义 每秒多少个请求 同时多少个请求
是否考虑时间
适合场景 QPS 限流 并发隔离
示例 每秒最多 100 个请求 同时最多 20 个请求执行

例如:

1
RateLimiter.create(100.0)

表示每秒 100 个许可。

1
new Semaphore(20)

表示最多 20 个线程同时进入。

真实系统中经常两者一起用:

1
2
RateLimiter 控制入口速率
Semaphore 控制同时执行数量

chapter 32:RateLimiter 和线程池队列的关系

线程池也能限制任务执行速度吗?

线程池主要控制:

  • 并发线程数;
  • 任务队列长度;
  • 拒绝策略。

它不能直接表达:

1
每秒最多提交 10 个任务。

RateLimiter 可以表达速率。

所以可以组合:

1
2
rateLimiter.acquire();
executor.submit(task);

这样任务提交速度被限制。

线程池负责执行资源管理。

RateLimiter 负责入口速率控制。

chapter 33:实战:限速调用第三方 API

假设第三方接口限制:

1
每秒最多 5 次调用

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.google.common.util.concurrent.RateLimiter;

public class ThirdPartyApiClient {

private final RateLimiter rateLimiter = RateLimiter.create(5.0);

public String call(String request) {
rateLimiter.acquire();

return doCall(request);
}

private String doCall(String request) {
System.out.println("调用第三方接口:" + request
+ ",time = " + System.currentTimeMillis());

return "ok";
}
}

如果不希望阻塞太久,可以用:

1
tryAcquire
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.google.common.util.concurrent.RateLimiter;

import java.util.concurrent.TimeUnit;

public class ThirdPartyApiClientWithTimeout {

private final RateLimiter rateLimiter = RateLimiter.create(5.0);

public String call(String request) {
boolean acquired = rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS);

if (!acquired) {
return "rate limited";
}

return doCall(request);
}

private String doCall(String request) {
return "ok";
}
}

chapter 34:实战:任务提交限流 + 线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import com.google.common.util.concurrent.RateLimiter;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class LimitedTaskExecutor {

private final RateLimiter rateLimiter;

private final ExecutorService executorService;

public LimitedTaskExecutor(double permitsPerSecond, int threads) {
this.rateLimiter = RateLimiter.create(permitsPerSecond);
this.executorService = Executors.newFixedThreadPool(threads);
}

public void submit(Runnable task) {
rateLimiter.acquire();

executorService.submit(task);
}

public void shutdown() {
executorService.shutdown();
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LimitedTaskExecutorDemo {

public static void main(String[] args) {
LimitedTaskExecutor executor = new LimitedTaskExecutor(5.0, 4);

for (int i = 1; i <= 20; i++) {
int taskId = i;

executor.submit(() -> {
System.out.println("执行任务:" + taskId
+ ",thread = "
+ Thread.currentThread().getName()
+ ",time = "
+ System.currentTimeMillis());
});
}

executor.shutdown();
}
}

这个例子中:

  • RateLimiter 控制任务提交速度;
  • 线程池控制任务执行并发。

chapter 35:实战:Spring Boot 单机接口限流拦截器

定义限流器:

1
2
3
4
5
6
7
8
9
10
11
12
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.stereotype.Component;

@Component
public class LocalApiRateLimiter {

private final RateLimiter rateLimiter = RateLimiter.create(100.0);

public boolean tryAcquire() {
return rateLimiter.tryAcquire();
}
}

拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

public class RateLimitInterceptor implements HandlerInterceptor {

private final LocalApiRateLimiter localApiRateLimiter;

public RateLimitInterceptor(LocalApiRateLimiter localApiRateLimiter) {
this.localApiRateLimiter = localApiRateLimiter;
}

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (!localApiRateLimiter.tryAcquire()) {
response.setStatus(429);
response.getWriter().write("too many requests");

return false;
}

return true;
}
}

注册拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

private final LocalApiRateLimiter localApiRateLimiter;

public WebConfig(LocalApiRateLimiter localApiRateLimiter) {
this.localApiRateLimiter = localApiRateLimiter;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RateLimitInterceptor(localApiRateLimiter))
.addPathPatterns("/api/**");
}
}

注意:

这是单机限流。多实例部署时,每个实例各限各的。

chapter 36:动态调整限流速率

RateLimiter 支持动态调整速率:

1
rateLimiter.setRate(200.0);

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.google.common.util.concurrent.RateLimiter;

public class DynamicRateLimiterDemo {

public static void main(String[] args) {
RateLimiter limiter = RateLimiter.create(10.0);

System.out.println("old rate = " + limiter.getRate());

limiter.setRate(20.0);

System.out.println("new rate = " + limiter.getRate());
}
}

这适合配置中心动态调整单机限流值。

例如:

  • 白天流量大,调高;
  • 下游系统异常,调低;
  • 灰度期间限制流量;
  • 压测期间动态调整。

真实项目中可以结合 Nacos、Apollo、Spring Cloud Config 等配置中心。

chapter 37:RateLimiter 的局限性

Guava RateLimiter 很好用,但有明显局限。

1. 单机限流

多个实例之间不共享状态。

如果有 10 个实例,每个实例限 100 QPS,总量可能是 1000 QPS。

2. 不支持分布式

不能全局限制用户、IP、接口的总请求数。

3. 不支持复杂规则

例如:

1
2
3
4
每个用户每分钟 10 次
每个 IP 每秒 5 次
不同接口不同规则
不同租户不同配额

需要自己封装。

4. 不提供监控指标

需要自己接入 Micrometer 或日志。

5. 不负责排队队列管理

acquire() 会阻塞当前线程。

高并发接口里大量阻塞可能拖垮线程池。

6. 不支持集群一致性

集群限流需要 Redis、网关、Sentinel 等方案。

chapter 38:什么时候不用 Guava RateLimiter

以下场景不建议只用 Guava RateLimiter:

1. 网关层限流

推荐使用网关限流。

例如:

  • Spring Cloud Gateway;
  • Nginx;
  • Envoy;
  • Kong;
  • APISIX。

2. 分布式全局限流

推荐 Redis Lua、Sentinel、Resilience4j、分布式限流服务。

3. 强可靠任务削峰

推荐 MQ。

例如 Kafka、RabbitMQ、RocketMQ。

4. 按用户、IP、租户多维度限流

Guava 可以封装,但复杂度高。

可能需要专门限流组件。

5. 高并发 Web 接口中长时间阻塞

不要大量使用 acquire() 阻塞 Tomcat/Netty 工作线程。

Web 接口更推荐快速失败或短超时等待。

chapter 39:限流返回什么状态码

HTTP 接口限流时,推荐返回:

1
429 Too Many Requests

响应内容可以是:

1
2
3
4
{
"code": "TOO_MANY_REQUESTS",
"message": "请求过于频繁,请稍后再试"
}

如果有重试建议,可以加响应头:

1
Retry-After: 1

不要返回 500。

限流不是服务器内部错误。

它是系统主动保护。

chapter 40:Monitor 和 RateLimiter 可以组合吗

可以。

例如一个任务处理系统:

  • RateLimiter 控制任务进入速度;
  • Monitor 控制队列容量;
  • 线程池控制执行并发。

结构:

1
请求 -> RateLimiter -> BoundedQueue(Monitor) -> Worker Thread

这样可以同时控制:

  • 单位时间进入多少任务;
  • 队列最多堆积多少任务;
  • 同时有多少线程执行任务。

这比单独使用某一个工具更稳。

chapter 41:完整案例:限流有界任务队列

需求:

  1. 任务提交速率每秒最多 5 个;
  2. 队列容量最多 10 个;
  3. 队列满时快速失败;
  4. Worker 从队列中取任务执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import com.google.common.util.concurrent.RateLimiter;

public class LimitedBoundedTaskQueue {

private final RateLimiter rateLimiter;

private final BoundedQueue<Runnable> queue;

public LimitedBoundedTaskQueue(double permitsPerSecond, int capacity) {
this.rateLimiter = RateLimiter.create(permitsPerSecond);
this.queue = new BoundedQueue<>(capacity);
}

public boolean submit(Runnable task) {
if (!rateLimiter.tryAcquire()) {
return false;
}

return queue.tryPut(task);
}

public Runnable take() throws InterruptedException {
return queue.take();
}
}

Worker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TaskWorker implements Runnable {

private final LimitedBoundedTaskQueue taskQueue;

public TaskWorker(LimitedBoundedTaskQueue taskQueue) {
this.taskQueue = taskQueue;
}

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Runnable task = taskQueue.take();

task.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
System.out.println("任务执行失败:" + e.getMessage());
}
}
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LimitedBoundedTaskQueueDemo {

public static void main(String[] args) {
LimitedBoundedTaskQueue queue = new LimitedBoundedTaskQueue(5.0, 10);

Thread worker = new Thread(new TaskWorker(queue), "task-worker");
worker.start();

for (int i = 1; i <= 100; i++) {
int taskId = i;

boolean success = queue.submit(() -> {
System.out.println("执行任务:" + taskId);
});

if (!success) {
System.out.println("任务提交失败:" + taskId);
}
}
}
}

这个例子同时用到了:

  • RateLimiter;
  • Monitor;
  • Guard;
  • 有界队列;
  • 快速失败。

chapter 42:并发和限流最佳实践

1. 能用 JDK 成熟并发容器,就不要自己造

例如有界队列可以用:

1
2
ArrayBlockingQueue
LinkedBlockingQueue

Monitor 适合学习和特殊条件控制。

2. 共享状态必须受保护

不要在多线程下裸写 ArrayList、HashMap、普通 int。

3. 临界区要小

锁内不要做远程调用和长时间 IO。

4. Web 接口限流优先快速失败

不要无限阻塞请求线程。

5. 单机限流不要误当分布式限流

Guava RateLimiter 只在当前 JVM 内生效。

6. 限流要有监控

至少统计:

  • 通过数;
  • 拒绝数;
  • 等待时间;
  • 当前配置速率。

7. 限流值要可配置

不要写死在代码里。

8. 限流不是降级的全部

还要考虑:

  • 熔断;
  • 超时;
  • 重试;
  • 隔离;
  • 队列;
  • 缓存;
  • MQ 削峰。

9. 任务系统要同时控制速率、并发和队列

只限制其中一个维度通常不够。

10. 中断要正确处理

捕获 InterruptedException 后要恢复中断标记:

1
Thread.currentThread().interrupt();

chapter 43:Monitor 常用 API 速查

API 说明
enter() 进入 Monitor,类似加锁
enterIf(Guard) 条件满足才进入,不等待
enterWhen(Guard) 等待条件满足后进入
tryEnter() 尝试进入
leave() 离开 Monitor,类似解锁
Guard.isSatisfied() 判断条件是否满足

常见结构:

1
2
3
4
5
6
7
monitor.enterWhen(guard);

try {
// logic
} finally {
monitor.leave();
}

chapter 44:RateLimiter 常用 API 速查

API 说明
RateLimiter.create(double permitsPerSecond) 创建平滑突发限流器
RateLimiter.create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) 创建预热限流器
acquire() 阻塞获取 1 个许可
acquire(int permits) 阻塞获取多个许可
tryAcquire() 立即尝试获取许可
tryAcquire(timeout, unit) 在指定时间内尝试获取许可
setRate(double permitsPerSecond) 动态调整速率
getRate() 获取当前速率

选择建议:

1
2
3
4
5
后台任务:acquire
用户接口:tryAcquire 或 tryAcquire(timeout)
冷启动系统:SmoothWarmingUp
单机轻量限流:RateLimiter
分布式限流:Redis / Sentinel / 网关

chapter 45:一句话总结

这一章的核心是:

Monitor 解决的是多线程条件同步问题,RateLimiter 解决的是单机速率限制问题。

MonitorGuard 表达进入临界区的条件。

它让:

1
锁 + 条件等待

变得更结构化。

适合理解:

  • 条件等待;
  • 有界队列;
  • enterWhen;
  • enterIf;
  • leave;
  • 与 synchronized、Lock、Condition 的区别。

RateLimiter 用于单机限流。

它可以:

  • 平滑请求;
  • 控制任务提交速率;
  • 控制第三方接口调用频率;
  • 支持阻塞获取许可;
  • 支持快速失败;
  • 支持预热限流;
  • 支持一定突发流量。

但要记住:

1
Guava RateLimiter 是单 JVM 工具,不是分布式限流系统。

如果你要做全局限流、网关限流、用户维度限流、租户维度限流、可靠削峰,应该考虑更完整的方案。

好的并发控制不是只会加锁。

好的限流也不是随手写一个 QPS 数字。

真正稳定的系统,往往会同时考虑:

1
2
3
4
5
6
7
8
9
速率
并发
队列
超时
降级
重试
隔离
监控
配置

Guava 的 Monitor 和 RateLimiter 是很好的入门工具。

但在生产系统里,要知道它们能解决什么,也要知道它们解决不了什么。

参考资料

  • Google Guava API: Monitor.
  • Google Guava API: Monitor.Guard.
  • Google Guava API: RateLimiter.
  • Google Guava API: SmoothRateLimiter.
  • Google Guava API: AsyncEventBus.
  • Java Documentation: synchronized.
  • Java Documentation: ReentrantLock.
  • Java Documentation: Condition.
  • Java Documentation: Semaphore.
  • Java Documentation: BlockingQueue.
  • Java Concurrency in Practice.
  • Martin Kleppmann. Designing Data-Intensive Applications.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


Guava:并发控制与限流
https://allendericdalexander.github.io/2026/04/01/java/utils/guava/08guava-concurrency-ratelimiter-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议