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);
}

曾经踩过的坑:有一次我把熔断阈值设得太低,结果某个服务稍微抖动了一下,熔断器就打开了。等那个服务恢复后,熔断器还在半开状态,导致大量请求走了降级逻辑。后来我把阈值调高了一些,并且加上了监控告警,情况就好多了。

好了,应用层优化这五个手段,每个都能单独用,但组合起来效果更好。比如线程池配合异步处理,限流配合熔断器。实际项目中,我建议先从线程池和异步入手,这两个改动成本低、见效快。等系统稳定了,再逐步加上限流和熔断器。

下一章我们聊聊数据库层的优化,那才是真正考验功底的地方。