欢迎你来读这篇博客,这篇博客主要是关于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 的核心概念有两个:
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()
|
它表示:
只有条件满足,线程才能继续进入关键区。
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()
|
必须保证:
在 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 2 3 4 5 6 7
| monitor.enter();
try { } finally { monitor.leave(); }
|
或者:
1 2 3 4 5 6 7
| monitor.enterWhen(guard);
try { } finally { monitor.leave(); }
|
不要这样:
1 2 3
| monitor.enter(); doSomething(); monitor.leave();
|
因为 doSomething() 可能抛异常。
一旦异常发生,leave() 不执行,就可能导致锁无法释放。
chapter 8:Monitor 实战:有界队列
下面实现一个有界队列。
需求:
- 队列有固定容量;
- 队列满时,生产者等待;
- 队列空时,消费者等待;
- 使用 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
| 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 快速失败
有时我们不希望队列满时一直等。
而是希望:
可以使用 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 { } finally { monitor.leave(); }
|
2. Guard 条件必须访问受 Monitor 保护的状态
例如:
这个状态应该在 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:漏桶算法思想
漏桶算法可以想象成一个桶。
请求像水一样进入桶。
桶底有一个孔,以固定速度漏水。
如果请求来得太快:
漏桶算法的核心是:
输出速率稳定。
适合保护下游系统。
比如数据库只能稳定处理每秒 100 次写入,那就让请求以每秒 100 的速度流出。
chapter 18:RateLimiter 和漏桶视角
Guava RateLimiter 不完全是传统漏桶实现。
但从调用者视角看:
会阻塞当前线程,让请求按固定速率通过。
例如:
1
| RateLimiter limiter = RateLimiter.create(2.0);
|
表示每秒 2 个许可。
也就是平均每 500ms 放行一个请求。
这看起来就像漏桶:
所以很多课程会从漏桶角度讲 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) 表示:
acquire() 会阻塞等待直到拿到许可。
返回值是等待的秒数。
chapter 20:acquire
acquire() 表示获取一个许可。
如果当前可以立即获取,就立即返回。
如果当前没有许可,就阻塞等待。
也可以一次获取多个许可:
这表示获取 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(50, TimeUnit.MILLISECONDS)
|
如果是后台任务,希望慢慢执行,可以用:
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 会变成:
分布式限流要用 Redis、网关、Sentinel 等方案。
chapter 25:令牌桶算法思想
令牌桶算法可以想象成一个桶。
系统按固定速率往桶里放令牌。
请求来了,需要先拿令牌。
拿到令牌才能通过。
1 2
| 系统生成令牌 -> 放入桶 请求到来 -> 拿令牌 -> 通过
|
如果桶里暂时积累了一些令牌,突发请求可以一次性拿走多个令牌。
这就是令牌桶和漏桶的区别。
漏桶
输出稳定,突发会被平滑。
令牌桶
平均速率受限,但允许一定突发。
例如:
如果系统空闲 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);
|
它表示:
突发能力来自 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 个许可。
表示最多 20 个线程同时进入。
真实系统中经常两者一起用:
1 2
| RateLimiter 控制入口速率 Semaphore 控制同时执行数量
|
chapter 32:RateLimiter 和线程池队列的关系
线程池也能限制任务执行速度吗?
线程池主要控制:
它不能直接表达:
RateLimiter 可以表达速率。
所以可以组合:
1 2
| rateLimiter.acquire(); executor.submit(task);
|
这样任务提交速度被限制。
线程池负责执行资源管理。
RateLimiter 负责入口速率控制。
chapter 33:实战:限速调用第三方 API
假设第三方接口限制:
代码:
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 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 2 3 4
| { "code": "TOO_MANY_REQUESTS", "message": "请求过于频繁,请稍后再试" }
|
如果有重试建议,可以加响应头:
不要返回 500。
限流不是服务器内部错误。
它是系统主动保护。
chapter 40:Monitor 和 RateLimiter 可以组合吗
可以。
例如一个任务处理系统:
- RateLimiter 控制任务进入速度;
- Monitor 控制队列容量;
- 线程池控制执行并发。
结构:
1
| 请求 -> RateLimiter -> BoundedQueue(Monitor) -> Worker Thread
|
这样可以同时控制:
- 单位时间进入多少任务;
- 队列最多堆积多少任务;
- 同时有多少线程执行任务。
这比单独使用某一个工具更稳。
chapter 41:完整案例:限流有界任务队列
需求:
- 任务提交速率每秒最多 5 个;
- 队列容量最多 10 个;
- 队列满时快速失败;
- 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 { } 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 解决的是单机速率限制问题。
Monitor 用 Guard 表达进入临界区的条件。
它让:
变得更结构化。
适合理解:
- 条件等待;
- 有界队列;
- 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.
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。