Java 后端高并发接口优化:从线程池、缓存到数据库

发布时间:2026/7/6 2:34:40
Java 后端高并发接口优化:从线程池、缓存到数据库 高并发优化不是简单地把某个参数调大也不是看到接口慢就立刻加缓存。一个 Java 后端接口从请求进入到响应返回可能经过网关、Tomcat、Controller、Service、缓存、数据库、消息队列和第三方服务。真正有效的优化需要先定位瓶颈再选择合适的手段。本文以典型 Spring Boot 服务为背景梳理一套高并发接口优化思路重点关注后端开发最常遇到的几个方向如何判断接口瓶颈在哪里。线程池应该怎么设置。缓存如何提升读性能又如何避免击穿和雪崩。数据库查询如何优化。写接口如何削峰填谷。如何通过限流和降级保护系统。一、先定位瓶颈不要凭感觉优化优化前要先回答三个问题慢在哪里为什么慢优化后怎么证明有效常见指标包括指标含义QPS每秒请求数RT请求响应时间P95 / P9995% 或 99% 请求的响应耗时CPU 使用率是否存在计算瓶颈JVM 堆内存是否频繁 GC数据库慢查询SQL 是否拖慢接口线程池队列长度是否出现任务堆积连接池使用率数据库或 HTTP 连接是否耗尽一个常见误区是只看平均响应时间。平均值容易掩盖长尾问题高并发场景更应该关注 P95 和 P99。例如一个接口平均 RT 是 80ms但 P99 是 2s说明大部分请求正常少量请求非常慢。此时要重点排查连接池等待、锁竞争、慢 SQL、GC 停顿或外部服务抖动。二、理解 Tomcat 线程模型Spring Boot 默认使用内嵌 Tomcat。每个 HTTP 请求进入服务后会由 Tomcat 工作线程处理。如果请求处理过程中大量阻塞在数据库、Redis 或第三方 HTTP 调用上Tomcat 线程就会被占住。常见配置如下server: tomcat: threads: max: 300 min-spare: 20 accept-count: 200 max-connections: 8192几个关键参数参数说明max-threads最大工作线程数min-spare-threads最小空闲线程数accept-count线程都忙时的等待队列长度max-connections最大连接数线程不是越多越好。线程数过高会增加上下文切换成本也会放大下游资源压力。比如数据库连接池只有 50 个连接Tomcat 线程开到 800并不会让数据库更快反而可能让大量请求阻塞等待连接。更合理的思路是入口线程数、业务线程池、数据库连接池、Redis 连接池、下游服务能力要整体匹配。三、业务线程池隔离不同类型任务如果接口中存在耗时但可以异步处理的任务例如发送通知、写操作日志、同步外部系统可以使用业务线程池隔离。Configuration public class ThreadPoolConfig { ​ Bean public Executor orderTaskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(500); executor.setThreadNamePrefix(order-task-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }使用线程池时要注意不同业务类型尽量使用不同线程池避免互相拖垮。队列不能无限大否则请求堆积时问题会被隐藏最终拖垮 JVM。拒绝策略要结合业务选择不能只依赖默认策略。线程池指标要接入监控包括活跃线程数、队列长度、拒绝次数。异步执行示例Service public class OrderNotifyService { ​ private final Executor orderTaskExecutor; ​ public OrderNotifyService(Executor orderTaskExecutor) { this.orderTaskExecutor orderTaskExecutor; } ​ public void notifyAfterOrderCreated(Long orderId) { orderTaskExecutor.execute(() - sendNotify(orderId)); } ​ private void sendNotify(Long orderId) { // 调用短信、站内信或第三方通知服务 } }注意异步不是万能的。用户必须立即知道结果的逻辑不能随便异步化例如支付扣款、库存扣减、权限校验等。四、缓存高并发读接口的第一道加速器对于读多写少的数据缓存通常是最有效的优化手段。典型场景包括商品详情。用户基础信息。字典配置。首页推荐模块。权限菜单。使用 Redis 缓存商品详情的示例Service public class ProductService { ​ private final StringRedisTemplate redisTemplate; private final ProductRepository productRepository; ​ public ProductService(StringRedisTemplate redisTemplate, ProductRepository productRepository) { this.redisTemplate redisTemplate; this.productRepository productRepository; } ​ public ProductDetailResponse getProductDetail(Long productId) { String cacheKey product:detail: productId; String cached redisTemplate.opsForValue().get(cacheKey); ​ if (cached ! null) { return JsonUtils.fromJson(cached, ProductDetailResponse.class); } ​ Product product productRepository.findById(productId) .orElseThrow(() - new BusinessException(PRODUCT_NOT_FOUND, 商品不存在)); ​ ProductDetailResponse response ProductConverter.toDetailResponse(product); redisTemplate.opsForValue().set(cacheKey, JsonUtils.toJson(response), Duration.ofMinutes(10)); return response; } }缓存能显著降低数据库压力但也会引入新的问题。五、缓存击穿、穿透和雪崩1. 缓存穿透缓存穿透指请求查询一个数据库中也不存在的数据。因为缓存没有命中每次请求都会打到数据库。解决方式对空结果做短时间缓存。对明显非法的 ID 做参数校验。使用布隆过滤器拦截不存在的 key。空值缓存示例if (product null) { redisTemplate.opsForValue().set(cacheKey, , Duration.ofMinutes(1)); throw new BusinessException(PRODUCT_NOT_FOUND, 商品不存在); }读取时区分空字符串if (.equals(cached)) { throw new BusinessException(PRODUCT_NOT_FOUND, 商品不存在); }2. 缓存击穿缓存击穿指某个热点 key 过期的一瞬间大量请求同时访问数据库。常见解决方式热点 key 不设置短过期时间通过后台任务刷新。使用互斥锁只有一个线程回源数据库。使用逻辑过期先返回旧值再异步刷新。互斥锁示例public ProductDetailResponse getProductDetail(Long productId) { String cacheKey product:detail: productId; String cached redisTemplate.opsForValue().get(cacheKey); if (StringUtils.hasText(cached)) { return JsonUtils.fromJson(cached, ProductDetailResponse.class); } ​ String lockKey lock: cacheKey; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofSeconds(5)); if (!Boolean.TRUE.equals(locked)) { Thread.sleep(50); return getProductDetail(productId); } ​ try { Product product productRepository.findById(productId) .orElseThrow(() - new BusinessException(PRODUCT_NOT_FOUND, 商品不存在)); ProductDetailResponse response ProductConverter.toDetailResponse(product); redisTemplate.opsForValue().set(cacheKey, JsonUtils.toJson(response), Duration.ofMinutes(10)); return response; } finally { redisTemplate.delete(lockKey); } }生产环境中要避免无限递归可以加最大重试次数或短暂降级。3. 缓存雪崩缓存雪崩指大量 key 在同一时间失效导致请求集中打到数据库。解决方式给过期时间增加随机抖动。热点数据分批预热。多级缓存比如本地缓存加 Redis。数据库层做好限流和降级保护。过期时间增加随机值long minutes 10 ThreadLocalRandom.current().nextLong(5); redisTemplate.opsForValue().set(cacheKey, value, Duration.ofMinutes(minutes));六、数据库优化让 SQL 先跑得足够好很多高并发问题本质是慢 SQL 在高流量下被放大。优化数据库可以从以下几个方面入手。1. 建立合适索引例如订单列表常按用户和创建时间查询SELECT id, order_no, amount, status, created_at FROM orders WHERE user_id ? ORDER BY created_at DESC LIMIT 20;可以考虑联合索引CREATE INDEX idx_orders_user_created_at ON orders(user_id, created_at DESC);索引设计要结合查询条件、排序字段和字段区分度不是每个字段都需要单独建索引。2. 避免查询过多字段不要在列表接口里随手写SELECT *。列表页只需要展示少数字段就只查需要的字段SELECT id, order_no, amount, status, created_at FROM orders WHERE user_id ? ORDER BY created_at DESC LIMIT 20;字段越少网络传输、内存占用和反序列化成本越低。3. 避免深分页深分页会让数据库扫描大量无用数据SELECT id, order_no FROM orders ORDER BY id DESC LIMIT 100000, 20;可以改成基于游标的分页SELECT id, order_no FROM orders WHERE id ? ORDER BY id DESC LIMIT 20;接口入参从pageNo变成lastIdpublic ListOrderListItemResponse listOrders(Long userId, Long lastId, int size) { return orderRepository.findByUserIdAndIdLessThanOrderByIdDesc(userId, lastId, PageRequest.of(0, size)); }对于移动端信息流、订单流水、消息列表游标分页通常比传统页码分页更稳定。七、写接口削峰消息队列与异步化高并发写接口最怕所有操作都同步落库、同步调用外部系统。比如下单接口里同时做创建订单。扣减库存。发优惠券。发送短信。推送站内信。写操作日志。同步 CRM。其中有些步骤必须同步完成有些则可以异步处理。可以同步完成核心链路校验用户 - 校验库存 - 创建订单 - 扣减库存 - 返回下单结果再通过消息队列处理非核心链路发送通知 - 写行为日志 - 同步外部系统 - 更新推荐画像发布事件示例Service public class OrderServiceImpl implements OrderService { private final OrderRepository orderRepository; private final ApplicationEventPublisher eventPublisher; public OrderServiceImpl(OrderRepository orderRepository, ApplicationEventPublisher eventPublisher) { this.orderRepository orderRepository; this.eventPublisher eventPublisher; } Transactional(rollbackFor Exception.class) public Long createOrder(CreateOrderRequest request) { Order order Order.create(request.getUserId(), request.getItems()); orderRepository.save(order); eventPublisher.publishEvent(new OrderCreatedEvent(order.getId())); return order.getId(); } }如果是单体应用可以先使用 Spring 事件或本地异步线程池如果是分布式系统可以使用 RocketMQ、Kafka、RabbitMQ 等消息队列。不过要注意消息队列带来的是最终一致性不是强一致性。涉及支付、库存、资金这类核心数据时需要设计幂等、重试、补偿和对账机制。八、幂等设计防止重复请求造成脏数据高并发环境下重复请求很常见用户连续点击按钮。前端超时后自动重试。网关或消息队列重复投递。第三方回调多次通知。幂等设计的目标是同一个业务请求执行一次和执行多次最终结果一致。常见方案使用唯一业务单号例如orderNo、requestId。数据库增加唯一索引。Redis 记录请求处理状态。消息消费端根据消息 ID 去重。以下单接口为例CREATE UNIQUE INDEX uk_orders_request_id ON orders(request_id);Service 中先基于requestId查询Transactional(rollbackFor Exception.class) public Long createOrder(CreateOrderRequest request) { OptionalOrder existingOrder orderRepository.findByRequestId(request.getRequestId()); if (existingOrder.isPresent()) { return existingOrder.get().getId(); } Order order Order.create(request.getRequestId(), request.getUserId(), request.getItems()); orderRepository.save(order); return order.getId(); }数据库唯一索引是最后一道防线。即使两个请求同时通过了查询也只能有一个插入成功。九、限流与降级保护系统边界当流量超过系统承载能力时系统需要主动保护自己。否则所有请求一起挤进来可能导致线程池耗尽、连接池打满、数据库被拖垮。常见限流维度按接口限流。按用户限流。按 IP 限流。按租户限流。按下游服务限流。常见限流算法算法特点固定窗口实现简单但窗口边界可能突刺滑动窗口比固定窗口平滑漏桶稳定匀速处理令牌桶允许一定突发流量本地单机可以使用 Guava RateLimiter分布式环境可以使用 Redis、Sentinel 或网关层限流。降级则是当系统压力过大或依赖不可用时返回一个可接受的兜底结果。例如推荐接口返回默认推荐。评论接口暂时隐藏评论。报表接口提示稍后再试。非核心数据使用缓存旧值。降级不是失败而是用有限能力保证核心链路可用。十、JVM 与 GC关注长尾延迟Java 后端在高并发场景下还要关注 JVM 状态尤其是内存分配和 GC。常见问题包括接口创建大量临时对象导致 Young GC 频繁。大对象进入老年代增加 Full GC 风险。本地缓存无限增长造成内存泄漏。日志打印过多增加对象分配和 IO 压力。排查时可以关注GC 次数 GC 停顿时间 堆内存使用率 对象分配速率 线程数量接口优化不仅是让平均耗时降低也要让长尾请求更稳定。一次 Full GC 可能让大量请求同时变慢最终表现为 P99 抖动。十一、可观测性优化必须能被验证没有监控的优化很容易变成玄学。建议至少具备以下能力接口维度的 QPS、RT、P95、P99。错误率和异常类型统计。数据库慢 SQL 记录。Redis 命中率和耗时。线程池活跃线程数、队列长度、拒绝次数。JVM GC 和堆内存指标。Trace 链路追踪定位一次请求经过哪些服务和依赖。一次优化前后应该能形成对比指标优化前优化后平均 RT320ms90msP95900ms180msP992.8s420ms数据库 QPS5000800缓存命中率0%92%只有能被量化优化才算真正闭环。十二、一个实战优化路径假设有一个商品详情接口在活动期间 QPS 从 200 提升到 5000数据库开始出现慢查询接口 P99 超过 3 秒。可以按下面顺序优化通过监控确认瓶颈在数据库查询。检查 SQL补充商品 ID、状态等必要索引。商品详情增加 Redis 缓存设置合理过期时间。对热点商品提前预热缓存。给缓存过期时间增加随机抖动避免大量 key 同时失效。对热点 key 使用逻辑过期或互斥锁避免缓存击穿。在网关或服务层增加限流保护数据库。接入接口、Redis、数据库和 JVM 监控观察 P95、P99 是否稳定。这个路径的重点是逐层缩小问题范围而不是一开始就改架构。十三、总结Java 后端高并发优化可以概括为几句话先观测再优化。线程池要隔离队列要有界拒绝策略要明确。读多写少优先考虑缓存但要处理穿透、击穿和雪崩。慢 SQL 在高并发下会被放大索引和查询结构必须扎实。写接口要区分核心链路和非核心链路必要时用消息队列削峰。重试、回调、消息消费都要考虑幂等。限流和降级是保护系统的必要手段。优化结果必须通过监控指标验证。高并发不是某一个技术点而是一套系统工程。真正稳定的后端服务靠的不是单次神奇调参而是从入口、线程、缓存、数据库、消息、监控到降级的整体设计。