场景升级:引入流量控制
- 在V1.1中,通过读写锁优化了系统的读性能,但留下了一个隐患,如果秒杀的“写”操作本身很耗时(比如需要调用外部API、复杂的数据库操作等),那么大量的写请求会在WriteLock.lock()处排起长队。这些排队的线程会持续占用着宝贵的服务器线程资源,当数量过多时,足以耗尽资源导致整个应用崩溃。
- V1.2的核心目标就是,在进入核心业务逻辑之前,先进行流量控制,只允许有限数量的请求进入,从而保护系统不被瞬时流量冲垮。
代码改写
- 加入semaphore信号量
- 加入
Thread.sleep(1000),模拟耗时的写操作。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
27public String processSeckill(Long productId, Long userId) {
try {
// 带超时的尝试获取:在指定时间内获取不到,就放弃,避免无限等待
if (!semaphore.tryAcquire(3, TimeUnit.SECONDS)) {
return "服务器繁忙,请稍后再试!";
}
// 这模拟了这样一种场景:比如,每个秒杀请求都需要先调用一个外部、
// 独立的、耗时1秒的API(如风控验证),这个API调用本身是可以并行的。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
writeLock.lock();
try {
// ... 之前的完整手动事务逻辑 ...
} finally {
writeLock.unlock();
}
} catch (InterruptedException e) {
// 线程在等待许可时被中断
Thread.currentThread().interrupt(); // 重新设置中断状态
return "请求被中断,请重试。";
} finally {
semaphore.release();
}
} - 最多10个线程可以同时获取到Semaphore许可。
- 这十个线程同时开始执行Thread.sleep(1000),模拟10个并行的慢操作。
- 1秒后,这10个线程几乎同时结束sleep,然后去竞争writeLock。
- WriteLock会确保他们一个一个地串行地完成数据库操作。
JMeter设置
- 保留写请求线程组
- 线程数:50
- Ramp-up Period : 1(模拟瞬时的大流量)
- 循环次数:1
压测结果与分析
- 预期结果:吞吐量应该为10/sec左右,即一秒钟内可以处理大约10个(由Semaphore信号量控制,而获取写锁之后的业务逻辑耗时极短)请求。
- 结果:吞吐量为19.8/sec

- 分析:信号量泄漏-代码中的逻辑bug
- 当其中的某一个线程获取许可失败,会
return "服务器繁忙,请稍后再试!,而无论try块中的代码是否正常结束,finally块中的代码都一定正常执行:semaphore.release();,也就是说,不管线程是否申请到了许可,都会执行finally块,即Semaphore内部的可用许可量可能会持续增加到10个以上。 - 使最后的结果显示——吞吐量:19.8/sec。
改进:代码中的逻辑bug
调整代码
- 修复:只有在成功获取到资源后,才能进入释放资源的finally块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 简化结构
public String processSeckill(...) {
if (semaphore.tryAcquire(...)) { // 1. 先过“信号量”这道门(10个并发名额)
try {
Thread.sleep(1000); // 2. 执行耗时1秒的【可并行】操作
writeLock.lock(); // 3. 再过“写锁”这道门(1个并发名额)
try {
// 4. 执行耗时极短的【串行】数据库操作
} finally {
writeLock.unlock();
}
} finally {
semaphore.release();
}
}
}
压测结果与分析
结果:吞吐量为11.1/sec

分析:
- 流程: 10个线程并行 sleep -> 串行 writeLock
- 总耗时 (处理50个请求): 50/10 批 * 1秒/批 ≈ 5秒
- 吞吐量: 50 / 5s ≈ 10/sec
当没有Semaphore时,吞吐量为20/sec。
- 流程: 50个线程并行 sleep -> 串行 writeLock
- 总耗时 (处理50个请求): 1秒 (并行sleep) + 50 * 数据库耗时 ≈ 2~3秒
- 吞吐量: 50 / ~2.5s ≈ 20/sec
证明了Semaphore流量控制的功能是生效的,它的作用不是提升性能,而是约束性能,防止过多的并发请求将系统资源耗尽,从而保证系统的稳定性。
学学八股
Semaphore
- 是JUC包提供的一个并发流程控制工具,在内部维护了一组“许可”,线程在执行前必须先获取一个许可,执行完毕后再归还许可。当许可被全部分发完毕后,其他没有获取到许可的线程就必须等待,直到有线程释放许可。
- 核心思想:通过有限的许可,来控制同一时间能够访问特定资源或执行特定代码块的线程数量。
- 核心方法:
acquire():阻塞式的获取一个许可。如果当前没有可用的许可,线程将进入休眠状态并排队等待,直到有其他线程调用release()。release():释放一个许可。信号量内部的许可计数会+1,如果此时有等待的线程,队列中的第一个线程将被唤醒。tryAcquire():非阻塞式的尝试获取许可。立即返回,成功为true,失败为false。tryAcquire(long timeout,TimeUnit unit):在指定时间内获取许可,如果超时仍未获取到,则返回false。
- 底层原理:和ReentrantLock一样,Semaphore的底层也是基于AQS构建
- state:AQS内部的int state 变量,在Semaphore中代表了当前可用的许可数量。
- 获取许可:对应AQS的共享模式获取,线程会通过CAS操作尝试将state-1,如果减1之后state的值仍然大于等于0,则获取成功。否则获取失败,线程会被打包成节点放入等待队列中并挂起、
- 释放许可:对应AQS的共享模式释放,线程会通过CAS操作将state+1,释放成功后,会唤醒等待队列中的后继线程。
- 关键特性与使用场景:
- Semaphore支持公平和非公平两种模式。
- 核心使用场景
- 流量控制/限流:防止瞬时大量请求冲垮下游服务。
- 管理有限的资源池:比如控制同时访问数据库的连接数,或者控制同时使用某个昂贵计算资源的任务数。

