4、应用层优化:线程池调优、异步处理机制、请求合并与批处理、限流降级策略、熔断器模式
应用层优化,说白了就是让代码本身跑得更聪明、更稳当。很多团队一上来就砸钱升级硬件,其实应用层的调优空间往往被严重低估了。我见过太多系统,CPU和内存都空着一大半,但请求就是处理不过来——问题出在代码的组织方式上。
这一章我们聊聊五个核心手段:线程池、异步处理、请求合并、限流降级、熔断器。它们不是孤立的,实际项目中经常组合使用。
4.1 线程池调优:别让线程成为瓶颈
线程池这东西,用好了是加速器,用不好就是定时炸弹。我记得有一次线上事故,就是因为线程池参数拍脑袋设的,结果高峰期线程数暴涨,直接把数据库连接池打满了。
核心参数怎么调?
| 参数 | 说明 | 我的建议 |
|---|---|---|
| corePoolSize | 核心线程数 | CPU密集型:N+1;IO密集型:2N(N为CPU核数) |
| maxPoolSize | 最大线程数 | 一般设为corePoolSize的2-4倍,别超过系统上限 |
| workQueue | 任务队列 | 有界队列!有界!有界!重要的事说三遍 |
| keepAliveTime | 空闲线程存活时间 | 60秒是个不错的起点 |
避坑指南:我曾经接手过一个CRM项目,线程池用的是无界队列。平时没事,一到月底报表生成,任务堆积到几百万,内存直接OOM。后来改成有界队列+合适的拒绝策略,问题就解决了。
代码示例:
// 我个人习惯这样配线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8, // corePoolSize
16, // maxPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 有界队列
new ThreadPoolExecutor.CallerRunsPolicy() // 让调用线程自己跑
);
小技巧:生产环境建议用ThreadPoolExecutor的构造函数,别用Executors工厂方法。Executors.newFixedThreadPool()默认用的是无界队列,你想想看,万一流量突增,后果不堪设想。
4.2 异步处理机制:别让主线程干等着
CRM系统里有很多非核心操作,比如发邮件、写日志、推送通知。这些事如果同步做,用户就得干等着。其实完全没必要。
我习惯把异步处理分成两种场景:
- 即时异步:用CompletableFuture或@Async注解,适合对响应时间敏感的操作
- 延迟异步:用消息队列(如RabbitMQ、Kafka),适合可以稍后处理的任务
举个例子:用户提交一个工单,系统需要保存数据、发邮件通知、更新缓存。如果同步做,响应时间可能超过2秒。改成异步后,主线程只做保存数据,邮件和缓存更新丢到线程池里,响应时间降到200毫秒以内。
注意:异步不是银弹。如果异步任务太多,线程池也会被打满。我曾经见过一个项目,所有操作都异步,结果异步线程池排队排到天荒地老。异步要分主次,核心链路还是同步处理比较好。
4.3 请求合并与批处理:减少网络开销
高并发场景下,每一次网络请求都有成本。如果能把多个小请求合并成一个大请求,吞吐量能提升不少。
常见的合并策略:
- 批量查询:把多个单条查询合并成一次批量查询。比如查100个用户的资料,别循环查100次,一次IN查询搞定
- 批量写入:攒够一定数量或一定时间,再批量写入数据库。我做过一个项目,用这种方式把写入吞吐量提升了5倍
- 请求合并器:用队列把短时间内的请求合并,再统一处理
代码示例(请求合并器):
// 一个简单的请求合并器思路
public class RequestMerger {
private final Queue<Request> queue = new ConcurrentLinkedQueue<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public RequestMerger() {
// 每100毫秒或攒够100个请求,就批量处理一次
scheduler.scheduleAtFixedRate(this::flush, 0, 100, TimeUnit.MILLISECONDS);
}
public void submit(Request req) {
queue.offer(req);
if (queue.size() >= 100) {
flush();
}
}
private void flush() {
List<Request> batch = new ArrayList<>();
while (!queue.isEmpty() && batch.size() < 100) {
batch.add(queue.poll());
}
if (!batch.isEmpty()) {
// 批量处理
processBatch(batch);
}
}
}
经验之谈:合并的时机和大小需要根据业务调整。合并得太频繁,效果不明显;合并得太慢,单个请求的延迟会变高。我一般建议从50个请求或50毫秒开始试,然后根据监控数据微调。
4.4 限流降级策略:保护系统不被冲垮
高并发系统最怕什么?怕流量突然暴涨,把系统打趴下。限流和降级就是系统的安全阀。
常用的限流算法:
| 算法 | 特点 | 适用场景 |
|---|---|---|
| 令牌桶 | 允许突发流量,平滑限流 | 大多数业务场景 |
| 漏桶 | 强制平滑流量,不允许突发 | 对流量稳定性要求高的场景 |
| 滑动窗口 | 精确控制时间窗口内的请求数 | 需要精确限流的场景 |
降级策略:限流是挡住流量,降级是牺牲非核心功能保核心功能。比如CRM系统里,客户列表查询是核心功能,但客户头像加载可以降级——用默认头像代替,等系统恢复后再加载。
我的做法:我会给每个接口打上等级标签。核心接口(如订单创建)限流阈值设高一些,非核心接口(如历史日志查询)阈值设低一些。当系统压力大时,自动降级非核心接口,保证核心接口的可用性。
4.5 熔断器模式:防止雪崩效应
熔断器模式,说白了就是给系统装个保险丝。当某个依赖服务连续失败时,熔断器打开,后续请求直接返回降级结果,不再调用那个服务。
熔断器的三个状态:
- 关闭:正常状态,请求正常通过
- 打开:失败率达到阈值,直接拒绝请求
- 半开:过了一段时间,放少量请求试探服务是否恢复
代码示例(用Hystrix实现熔断):
@HystrixCommand(
fallbackMethod = "getCustomerFallback",
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000")
}
)
public Customer getCustomer(Long id) {
// 调用远程服务
return customerServiceClient.getCustomer(id);
}
public Customer getCustomerFallback(Long id) {
// 降级逻辑:返回缓存数据或默认值
return cache.get(id);
}
曾经踩过的坑:有一次我把熔断阈值设得太低,结果某个服务稍微抖动了一下,熔断器就打开了。等那个服务恢复后,熔断器还在半开状态,导致大量请求走了降级逻辑。后来我把阈值调高了一些,并且加上了监控告警,情况就好多了。
好了,应用层优化这五个手段,每个都能单独用,但组合起来效果更好。比如线程池配合异步处理,限流配合熔断器。实际项目中,我建议先从线程池和异步入手,这两个改动成本低、见效快。等系统稳定了,再逐步加上限流和熔断器。
下一章我们聊聊数据库层的优化,那才是真正考验功底的地方。