Spring Boot 微服务性能优化完全指南

Spring Boot 微服务性能优化完全指南


一、性能优化全景图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        性能优化四大维度                           │
├────────────────┬────────────────┬───────────────┬───────────────┤
│   查询优化      │   缓存优化     │   并发优化     │   网络优化    │
│                │               │              │              │
│ • 分页查询     │ • Redis 缓存   │ • 异步处理    │ • 连接复用    │
│ • 索引设计     │ • 本地缓存     │ • 线程池      │ • 批量查询    │
│ • SQL 优化     │ • 多级缓存     │ • 分布式锁    │ • 超时控制    │
│ • MyBatis 缓存 │ • 缓存预热     │ • 消息队列    │ • 压缩传输    │
└────────────────┴────────────────┴───────────────┴───────────────┘

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、查询优化

2.1 分页查询

问题:一次性加载大量数据
java 复制代码
// ❌ 危险:如果表有百万数据,直接 OOM 或超慢
List<Order> allOrders = orderMapper.selectAll();
方案一:使用 PageHelper(MyBatis 分页插件)
java 复制代码
// Maven 依赖
// <dependency>
//   <groupId>com.github.pagehelper</groupId>
//   <artifactId>pagehelper-spring-boot-starter</artifactId>
//   <version>2.1.0</version>
// </dependency>

@Service
public class OrderServiceImpl implements OrderService {

  @Override
  public PageResult<OrderResponse> listOrders(OrderQueryRequest request) {
    // 设置分页参数(页码从1开始,每页20条)
    PageHelper.startPage(request.getPageNum(), request.getPageSize());

    // 紧跟着的第一个查询会被自动分页
    List<Order> orders = orderMapper.selectByCondition(request);

    // 获取分页信息
    PageInfo<Order> pageInfo = new PageInfo<>(orders);

    // 组装返回
    PageResult<OrderResponse> result = new PageResult<>();
    result.setTotal(pageInfo.getTotal());
    result.setPages(pageInfo.getPages());
    result.setPageNum(pageInfo.getPageNum());
    result.setPageSize(pageInfo.getPageSize());
    result.setList(orders.stream().map(this::toResponse).toList());
    return result;
  }
}

通用分页结果类:

java 复制代码
@Data
public class PageResult<T> {
  private long total;       // 总记录数
  private int pages;        // 总页数
  private int pageNum;      // 当前页码
  private int pageSize;     // 每页条数
  private List<T> list;     // 数据列表
}
方案二:手动分页(大数据量场景更优)
java 复制代码
// Mapper
@Select("SELECT * FROM t_order WHERE user_id = #{userId} " +
        "ORDER BY id DESC LIMIT #{offset}, #{limit}")
List<Order> selectPage(@Param("userId") Long userId,
                       @Param("offset") Integer offset,
                       @Param("limit") Integer limit);

// Service
public List<Order> listOrders(Long userId, int pageNum, int pageSize) {
  int offset = (pageNum - 1) * pageSize;
  return orderMapper.selectPage(userId, offset, pageSize);
}
方案三:游标分页(深分页优化)

LIMIT 100000, 20 这种深分页出现性能问题时:

sql 复制代码
-- ❌ 慢:MySQL 需要扫描前 100000 条再丢弃
SELECT * FROM t_order ORDER BY id DESC LIMIT 100000, 20;

-- ✅ 快:利用索引直接定位
SELECT * FROM t_order WHERE id < #{lastId} ORDER BY id DESC LIMIT 20;
// 游标分页接口
@GetMapping("/orders")
public Result<List<OrderResponse>> listOrders(
    @RequestParam(required = false) Long lastId,  // 上一页最后一条的ID
    @RequestParam(defaultValue = "20") int size) {
  
  List<Order> orders;
  if (lastId == null) {
    orders = orderMapper.selectFirstPage(size);
  } else {
    orders = orderMapper.selectAfter(lastId, size);
  }
  return Result.success(orders.stream().map(this::toResponse).toList());
}

2.2 索引设计

索引原则
原则 说明
为 WHERE 条件字段建索引 WHERE status = 'PAID' → status 需要索引
为 ORDER BY 字段建索引 ORDER BY create_time DESC → create_time 需要索引
为 JOIN 字段建索引 ON a.user_id = b.id → user_id 需要索引
联合索引遵循最左前缀 INDEX(a, b, c) → 可命中 aa,ba,b,c
区分度高的字段放前面 INDEX(status, user_id) → user_id 区分度更高应放前面
避免过多索引 每个索引都增加写入开销,控制在 5-6 个以内
常见场景示例
sql 复制代码
-- 场景1:按用户查订单,按时间排序
-- 查询:WHERE user_id = ? ORDER BY create_time DESC
CREATE INDEX idx_user_time ON t_order(user_id, create_time DESC);

-- 场景2:按状态+时间范围查询
-- 查询:WHERE status = ? AND create_time BETWEEN ? AND ?
CREATE INDEX idx_status_time ON t_order(status, create_time);

-- 场景3:唯一业务编号查询
-- 查询:WHERE order_no = ?
CREATE UNIQUE INDEX uk_order_no ON t_order(order_no);

-- 场景4:组合条件查询
-- 查询:WHERE user_id = ? AND status = ? AND product_id = ?
CREATE INDEX idx_user_status_product ON t_order(user_id, status, product_id);
索引失效的常见场景
sql 复制代码
-- ❌ 对索引字段使用函数
SELECT * FROM t_order WHERE DATE(create_time) = '2026-06-16';
-- ✅ 改为范围查询
SELECT * FROM t_order WHERE create_time >= '2026-06-16' AND create_time < '2026-06-17';

-- ❌ 隐式类型转换(字段是 VARCHAR,传入 INT)
SELECT * FROM t_order WHERE order_no = 12345;
-- ✅ 类型一致
SELECT * FROM t_order WHERE order_no = '12345';

-- ❌ LIKE 左模糊
SELECT * FROM t_order WHERE order_no LIKE '%ABC';
-- ✅ LIKE 右模糊可命中索引
SELECT * FROM t_order WHERE order_no LIKE 'ABC%';

-- ❌ OR 条件中有非索引字段
SELECT * FROM t_order WHERE user_id = 1 OR remark = 'test';
-- ✅ 用 UNION 替代
SELECT * FROM t_order WHERE user_id = 1
UNION
SELECT * FROM t_order WHERE remark = 'test';

-- ❌ 联合索引不满足最左前缀
-- INDEX(a, b, c),只查 b=1 无法命中
SELECT * FROM t_order WHERE b = 1;

2.3 SQL 优化

sql 复制代码
-- ❌ SELECT * 查出所有字段(包括大文本)
SELECT * FROM t_order WHERE user_id = 1001;

-- ✅ 只查需要的字段
SELECT id, order_no, status, total_amount, create_time 
FROM t_order WHERE user_id = 1001;

-- ❌ 在循环中执行 N 条 SQL(N+1 问题)
-- Java: for (Order o : orders) { mapper.selectItems(o.getId()); }

-- ✅ 一次性批量查询
SELECT * FROM t_order_item WHERE order_id IN (1, 2, 3, 4, 5);

-- ❌ 大表 COUNT(*)
SELECT COUNT(*) FROM t_order;  -- 全表扫描

-- ✅ 使用预估值或缓存计数
SELECT TABLE_ROWS FROM information_schema.TABLES 
WHERE TABLE_NAME = 't_order';  -- 近似值

2.4 MyBatis 缓存

一级缓存(SqlSession 级别)--- 默认开启
yaml 复制代码
mybatis:
  configuration:
    local-cache-scope: STATEMENT  # SESSION(默认) 或 STATEMENT
行为
SESSION 同一个 SqlSession 内相同查询命中缓存(Spring 中基本无效,每次请求新 Session)
STATEMENT 每次执行完清除缓存(最安全,推荐)
二级缓存(Mapper 级别)--- 需手动开启
xml 复制代码
<!-- OrderMapper.xml -->
<mapper namespace="com.example.dao.OrderMapper">
  <!-- 开启二级缓存 -->
  <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
  
  <!-- 此 Mapper 的所有 SELECT 结果都会被缓存 -->
  <select id="selectById" resultMap="BaseResultMap">
    SELECT * FROM t_order WHERE id = #{id}
  </select>
</mapper>

注意:MyBatis 二级缓存在分布式环境下有一致性问题(各节点缓存不同步),生产环境推荐用 Redis 替代。


三、缓存优化

3.1 缓存架构

复制代码
请求
  ↓
┌─────────────────────┐
│  本地缓存 (Caffeine) │  ← 命中率高的热数据,毫秒级响应
│  TTL: 30s-5min      │
└──────────┬──────────┘
           ↓ 未命中
┌─────────────────────┐
│  分布式缓存 (Redis)  │  ← 所有节点共享,毫秒级响应
│  TTL: 5min-1h       │
└──────────┬──────────┘
           ↓ 未命中
┌─────────────────────┐
│  数据库 (MySQL)      │  ← 最终数据源,写入后更新缓存
│                     │
└─────────────────────┘

3.2 Redis 缓存完整示例

配置
yaml 复制代码
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: ""
      database: 0
      lettuce:
        pool:
          max-active: 20
          max-idle: 10
          min-idle: 5
          max-wait: 3000ms
Redis 工具类
java 复制代码
@Component
public class RedisUtil {

  private final StringRedisTemplate redisTemplate;

  public RedisUtil(StringRedisTemplate redisTemplate) {
    this.redisTemplate = redisTemplate;
  }

  /**
   * 设置缓存(带过期时间).
   */
  public void set(String key, Object value, Duration ttl) {
    redisTemplate.opsForValue().set(key, JsonUtil.toJson(value), ttl);
  }

  /**
   * 获取缓存.
   */
  public <T> T get(String key, Class<T> clazz) {
    String json = redisTemplate.opsForValue().get(key);
    return json != null ? JsonUtil.fromJson(json, clazz) : null;
  }

  /**
   * 获取缓存(List 类型).
   */
  public <T> List<T> getList(String key, Class<T> elementClass) {
    String json = redisTemplate.opsForValue().get(key);
    return json != null ? JsonUtil.fromJsonList(json, elementClass) : null;
  }

  /**
   * 删除缓存.
   */
  public void delete(String key) {
    redisTemplate.delete(key);
  }

  /**
   * 批量删除(模糊匹配).
   */
  public void deleteByPattern(String pattern) {
    Set<String> keys = redisTemplate.keys(pattern);
    if (keys != null && !keys.isEmpty()) {
      redisTemplate.delete(keys);
    }
  }
}
Service 中使用缓存
java 复制代码
@Slf4j
@Service
public class ProductServiceImpl implements ProductService {

  private static final String CACHE_KEY_PREFIX = "product:";
  private static final Duration CACHE_TTL = Duration.ofMinutes(30);
  private static final Duration CACHE_NULL_TTL = Duration.ofMinutes(2); // 空值缓存

  private final ProductMapper productMapper;
  private final RedisUtil redisUtil;

  @Override
  public ProductResponse getProduct(Long productId) {
    String cacheKey = CACHE_KEY_PREFIX + productId;

    // 1. 查缓存
    ProductResponse cached = redisUtil.get(cacheKey, ProductResponse.class);
    if (cached != null) {
      // 判断是否为空值缓存(防缓存穿透)
      if (cached.getProductId() == null) {
        return null;
      }
      return cached;
    }

    // 2. 查数据库
    Product product = productMapper.selectById(productId);

    // 3. 写缓存
    if (product != null) {
      ProductResponse response = toResponse(product);
      redisUtil.set(cacheKey, response, CACHE_TTL);
      return response;
    } else {
      // 缓存空值,防止缓存穿透
      redisUtil.set(cacheKey, new ProductResponse(), CACHE_NULL_TTL);
      return null;
    }
  }

  @Override
  @Transactional
  public void updateProduct(Long productId, UpdateProductRequest request) {
    productMapper.update(productId, request);
    // 更新后删除缓存(下次查询时重建)
    redisUtil.delete(CACHE_KEY_PREFIX + productId);
  }
}

3.3 缓存常见问题及解决

问题 场景 解决方案
缓存穿透 查询不存在的数据,每次都打到 DB 缓存空值 + 布隆过滤器
缓存击穿 热点 key 过期,大量请求同时打到 DB 互斥锁 + 逻辑过期
缓存雪崩 大量 key 同时过期 TTL 加随机偏移 + 预热
数据不一致 更新 DB 后缓存还是旧值 先更新 DB,再删缓存
缓存击穿:互斥锁方案
java 复制代码
public ProductResponse getProductWithLock(Long productId) {
  String cacheKey = CACHE_KEY_PREFIX + productId;
  String lockKey = "lock:" + cacheKey;

  // 1. 查缓存
  ProductResponse cached = redisUtil.get(cacheKey, ProductResponse.class);
  if (cached != null) {
    return cached;
  }

  // 2. 获取互斥锁(只有一个线程去查 DB)
  boolean locked = redisTemplate.opsForValue()
      .setIfAbsent(lockKey, "1", Duration.ofSeconds(10));

  if (locked) {
    try {
      // 双检:获取锁后再查一次缓存(可能被其他线程已重建)
      cached = redisUtil.get(cacheKey, ProductResponse.class);
      if (cached != null) {
        return cached;
      }

      // 查 DB 并重建缓存
      Product product = productMapper.selectById(productId);
      ProductResponse response = toResponse(product);
      redisUtil.set(cacheKey, response, CACHE_TTL);
      return response;
    } finally {
      redisTemplate.delete(lockKey);
    }
  } else {
    // 未获取锁,等待后重试
    try {
      Thread.sleep(50);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
    return getProductWithLock(productId);
  }
}
缓存雪崩:TTL 加随机偏移
java 复制代码
// ❌ 所有 key 固定 30 分钟过期 → 可能同时失效
redisUtil.set(key, value, Duration.ofMinutes(30));

// ✅ TTL 加随机偏移(30分钟 ± 5分钟)
Duration ttl = Duration.ofMinutes(30)
    .plusSeconds(ThreadLocalRandom.current().nextInt(0, 300));
redisUtil.set(key, value, ttl);

3.4 本地缓存(Caffeine)

适用于变化不频繁、访问极频繁的数据(如配置、字典):

java 复制代码
// 依赖:com.github.ben-manes.caffeine:caffeine:3.1.8

@Configuration
public class CacheConfig {

  @Bean
  public Cache<String, Object> localCache() {
    return Caffeine.newBuilder()
        .maximumSize(10000)              // 最多缓存 10000 个 key
        .expireAfterWrite(Duration.ofMinutes(5))  // 写入后 5 分钟过期
        .recordStats()                   // 记录命中率等统计
        .build();
  }
}

// 使用
@Service
public class DictServiceImpl {

  private final Cache<String, Object> localCache;
  private final DictMapper dictMapper;

  public String getDictLabel(String type, String code) {
    String key = "dict:" + type + ":" + code;

    // 先查本地缓存
    Object cached = localCache.getIfPresent(key);
    if (cached != null) {
      return (String) cached;
    }

    // 查 DB
    String label = dictMapper.selectLabel(type, code);
    if (label != null) {
      localCache.put(key, label);
    }
    return label;
  }
}

四、并发优化

4.1 异步处理(消息队列)

将非核心逻辑从主流程中剥离,异步执行:

复制代码
同步(优化前):创建订单 → 扣库存 → 发短信 → 发邮件 → 更新积分 → 返回(800ms)
异步(优化后):创建订单 → 扣库存 → 返回(200ms)
                          ↓ 发消息到 MQ
              异步:发短信 / 发邮件 / 更新积分
@Service
public class OrderServiceImpl implements OrderService {

  private final KafkaTemplate<String, String> kafkaTemplate;

  @Override
  @Transactional
  public OrderResponse createOrder(CreateOrderRequest request) {
    // 核心逻辑(同步)
    Order order = saveOrder(request);
    deductStock(request);

    // 非核心逻辑(异步发消息)
    OrderCreatedEvent event = new OrderCreatedEvent(
        order.getOrderNo(), order.getUserId(), order.getTotalAmount());
    kafkaTemplate.send("order-created-topic", JsonUtil.toJson(event));

    return toResponse(order);
  }
}

// 异步消费者
@Component
public class OrderEventConsumer {

  @KafkaListener(topics = "order-created-topic")
  public void onOrderCreated(String message) {
    OrderCreatedEvent event = JsonUtil.fromJson(message, OrderCreatedEvent.class);

    // 这些操作即使失败也不影响主流程
    smsService.sendOrderNotification(event.getUserId());
    emailService.sendOrderConfirmation(event.getUserId());
    pointsService.addPoints(event.getUserId(), event.getAmount());
  }
}

4.2 线程池配置

Spring 异步线程池
java 复制代码
@Configuration
@EnableAsync
public class ThreadPoolConfig {

  /**
   * 通用异步线程池.
   */
  @Bean("asyncExecutor")
  public Executor asyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10);       // 核心线程数
    executor.setMaxPoolSize(50);        // 最大线程数
    executor.setQueueCapacity(200);     // 队列容量
    executor.setKeepAliveSeconds(60);   // 非核心线程空闲存活时间
    executor.setThreadNamePrefix("async-");
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.setTaskDecorator(new MdcTaskDecorator()); // 传播 RequestId
    executor.initialize();
    return executor;
  }
}

// 使用
@Async("asyncExecutor")
public void sendNotificationAsync(Long userId, String message) {
  // 异步执行,不阻塞主线程
  notificationClient.send(userId, message);
}
线程池参数选择
参数 CPU 密集型任务 IO 密集型任务
核心线程数 CPU 核数 + 1 CPU 核数 × 2
最大线程数 CPU 核数 + 1 CPU 核数 × 4
队列容量 较小(快速拒绝) 较大(缓冲 IO 等待)
java 复制代码
// 获取 CPU 核数
int cpuCores = Runtime.getRuntime().availableProcessors();

// IO 密集型(如 HTTP 调用、DB 查询)
executor.setCorePoolSize(cpuCores * 2);
executor.setMaxPoolSize(cpuCores * 4);
executor.setQueueCapacity(500);
拒绝策略
策略 行为 适用场景
AbortPolicy 抛异常 不允许丢失任务
CallerRunsPolicy 调用者线程执行 降级但不丢失(推荐)
DiscardPolicy 静默丢弃 可容忍丢失
DiscardOldestPolicy 丢弃队列最早的 只关心最新任务

4.3 分布式锁(Redis)

防止多个实例并发执行同一个操作:

java 复制代码
@Component
public class RedisDistributedLock {

  private final StringRedisTemplate redisTemplate;

  /**
   * 尝试获取锁.
   *
   * @param lockKey 锁的 key
   * @param requestId 请求标识(用于释放时校验)
   * @param expireTime 锁过期时间
   * @return 是否获取成功
   */
  public boolean tryLock(String lockKey, String requestId, Duration expireTime) {
    Boolean success = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, requestId, expireTime);
    return Boolean.TRUE.equals(success);
  }

  /**
   * 释放锁(Lua 脚本保证原子性).
   */
  public boolean unlock(String lockKey, String requestId) {
    String script = """
        if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
        """;
    Long result = redisTemplate.execute(
        new DefaultRedisScript<>(script, Long.class),
        List.of(lockKey), requestId);
    return Long.valueOf(1).equals(result);
  }
}

使用示例:

java 复制代码
@Service
public class StockServiceImpl {

  private final RedisDistributedLock distributedLock;

  /**
   * 扣减库存(加分布式锁防止超卖).
   */
  public void deductStock(Long productId, Integer qty) {
    String lockKey = "lock:stock:" + productId;
    String requestId = UUID.randomUUID().toString();

    boolean locked = distributedLock.tryLock(lockKey, requestId, Duration.ofSeconds(10));
    if (!locked) {
      throw new BusinessException("系统繁忙,请稍后重试");
    }

    try {
      // 查询当前库存
      int currentStock = stockMapper.getStock(productId);
      if (currentStock < qty) {
        throw new BusinessException("库存不足");
      }
      // 扣减库存
      stockMapper.deduct(productId, qty);
    } finally {
      distributedLock.unlock(lockKey, requestId);
    }
  }
}

注意:简单场景用 Redis 锁即可。如果需要可重入、自动续期等高级功能,推荐使用 Redisson:

java 复制代码
RLock lock = redissonClient.getLock("lock:stock:" + productId);
lock.lock(10, TimeUnit.SECONDS);
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

4.4 数据库层面的并发控制

sql 复制代码
-- 乐观锁(通过版本号,无锁等待)
UPDATE t_stock 
SET qty = qty - #{qty}, version = version + 1 
WHERE product_id = #{productId} AND version = #{version} AND qty >= #{qty};
-- affected == 0 则重试或报错

-- CAS 无版本号写法(利用条件判断)
UPDATE t_stock 
SET qty = qty - #{qty} 
WHERE product_id = #{productId} AND qty >= #{qty};
-- affected == 0 表示库存不足
// 乐观锁重试
public void deductWithRetry(Long productId, Integer qty, int maxRetry) {
  for (int i = 0; i < maxRetry; i++) {
    Stock stock = stockMapper.selectById(productId);
    if (stock.getQty() < qty) {
      throw new BusinessException("库存不足");
    }
    int affected = stockMapper.deductWithVersion(productId, qty, stock.getVersion());
    if (affected > 0) {
      return;  // 成功
    }
    // 失败则重试
    log.warn("乐观锁冲突, 重试第{}次, productId={}", i + 1, productId);
  }
  throw new BusinessException("系统繁忙,请稍后重试");
}

五、网络优化

5.1 HTTP 连接复用(OkHttp 连接池)

问题

每次 HTTP 请求都新建 TCP 连接(三次握手 + TLS),高频调用下延迟严重。

解决:连接池复用
java 复制代码
@Configuration
public class OkHttpConfig {

  @Bean
  public OkHttpClient okHttpClient() {
    return new OkHttpClient.Builder()
        // 连接池:最多 100 个空闲连接,存活 5 分钟
        .connectionPool(new ConnectionPool(100, 5, TimeUnit.MINUTES))
        // 连接超时
        .connectTimeout(5, TimeUnit.SECONDS)
        // 读取超时
        .readTimeout(10, TimeUnit.SECONDS)
        // 写入超时
        .writeTimeout(10, TimeUnit.SECONDS)
        // 失败重试
        .retryOnConnectionFailure(true)
        .build();
  }
}
Feign + OkHttp 集成
yaml 复制代码
spring:
  cloud:
    openfeign:
      okhttp:
        enabled: true       # 使用 OkHttp 替代默认的 URLConnection
      client:
        config:
          default:
            connect-timeout: 5000
            read-timeout: 10000
复制代码
<dependency>
  <groupId>io.github.openfeign</groupId>
  <artifactId>feign-okhttp</artifactId>
</dependency>

5.2 超时时间设置

超时分层设计
复制代码
┌─────────────────────────────────────────────┐
│  网关层超时(最外层)         30s             │
│  ┌───────────────────────────────────────┐  │
│  │  Feign 调用超时           10-15s       │  │
│  │  ┌─────────────────────────────────┐  │  │
│  │  │  HTTP 连接超时          3-5s     │  │  │
│  │  │  ┌───────────────────────────┐  │  │  │
│  │  │  │  数据库超时       1-3s     │  │  │  │
│  │  │  └───────────────────────────┘  │  │  │
│  │  └─────────────────────────────────┘  │  │
│  └───────────────────────────────────────┘  │
└─────────────────────────────────────────────┘

原则:内层超时 < 外层超时,确保内层超时能被外层感知并处理。

各层超时配置
yaml 复制代码
# Feign 调用超时
spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            connect-timeout: 5000    # 连接超时 5s
            read-timeout: 10000      # 读取超时 10s
          # 针对慢服务单独设置
          report-service:
            read-timeout: 30000      # 报表服务可能慢,设 30s

# 数据库连接超时(HikariCP)
spring:
  datasource:
    hikari:
      connection-timeout: 3000       # 获取连接超时 3s
      validation-timeout: 2000       # 连接验证超时 2s

# Redis 超时
spring:
  data:
    redis:
      timeout: 2000                  # 命令执行超时 2s
      lettuce:
        pool:
          max-wait: 3000ms           # 获取连接超时 3s

# Tomcat 请求超时
server:
  tomcat:
    connection-timeout: 5000         # 连接超时 5s
  servlet:
    session:
      timeout: 30m
超时后如何处理?
java 复制代码
@Slf4j
@Service
public class OrderServiceImpl {

  @Override
  public OrderResponse createOrder(CreateOrderRequest request) {
    // 调用库存服务(可能超时)
    try {
      stockFeign.deduct(request.getProductId(), request.getQuantity());
    } catch (FeignException.GatewayTimeout e) {
      // 超时不代表失败!可能对方已执行成功
      log.warn("库存服务调用超时, 进入异步确认流程, productId={}", request.getProductId());
      // 方案1:异步查询执行结果
      asyncConfirmService.checkStockDeduction(request);
      // 方案2:订单标记为"处理中",等待回调确认
      return createPendingOrder(request);
    } catch (FeignException e) {
      // 明确失败(4xx/5xx),可以直接报错
      throw new BusinessException("库存扣减失败: " + e.getMessage());
    }
  }
}

5.3 批量查询

问题:循环调用(N+1)
java 复制代码
// ❌ N+1 问题:查 100 个订单,每个订单单独查商品信息 = 101 次查询
List<Order> orders = orderMapper.selectByUserId(userId);
for (Order order : orders) {
  Product product = productMapper.selectById(order.getProductId()); // 循环内查询
  order.setProductName(product.getName());
}
解决:批量查询 + 内存组装
java 复制代码
// ✅ 批量查询,只需 2 次查询
List<Order> orders = orderMapper.selectByUserId(userId);

// 收集所有商品ID,一次性批量查询
Set<Long> productIds = orders.stream()
    .map(Order::getProductId)
    .collect(Collectors.toSet());
List<Product> products = productMapper.selectByIds(productIds);

// 转为 Map 方便查找
Map<Long, Product> productMap = products.stream()
    .collect(Collectors.toMap(Product::getId, Function.identity()));

// 内存中组装
for (Order order : orders) {
  Product product = productMap.get(order.getProductId());
  if (product != null) {
    order.setProductName(product.getName());
  }
}
批量 Feign 调用
java 复制代码
// ❌ 循环调用其他服务
for (Long productId : productIds) {
  Product p = productFeign.getProduct(productId);  // N 次 HTTP
}

// ✅ 提供批量接口,一次调用
List<Product> products = productFeign.batchGetProducts(productIds);  // 1 次 HTTP

对应 Feign 接口设计:

java 复制代码
@FeignClient(name = "product-service")
public interface ProductFeign {

  // 单个查询
  @GetMapping("/api/product/{id}")
  Result<Product> getProduct(@PathVariable Long id);

  // 批量查询(推荐)
  @PostMapping("/api/product/batch")
  Result<List<Product>> batchGetProducts(@RequestBody List<Long> ids);
}
MyBatis 批量查询
xml 复制代码
<!-- 批量查询 -->
<select id="selectByIds" resultMap="BaseResultMap">
  SELECT * FROM t_product
  WHERE id IN
  <foreach collection="ids" item="id" open="(" separator="," close=")">
    #{id}
  </foreach>
  AND deleted = 0
</select>

<!-- 批量插入 -->
<insert id="batchInsert">
  INSERT INTO t_order_item (order_id, product_id, qty, price)
  VALUES
  <foreach collection="items" item="item" separator=",">
    (#{item.orderId}, #{item.productId}, #{item.qty}, #{item.price})
  </foreach>
</insert>

注意 :MySQL 的 IN 子句参数不宜超过 1000 个。大量 ID 时需要分批:

java 复制代码
// 每批最多 500 个
List<List<Long>> batches = Lists.partition(new ArrayList<>(productIds), 500);
List<Product> allProducts = new ArrayList<>();
for (List<Long> batch : batches) {
  allProducts.addAll(productMapper.selectByIds(batch));
}

5.4 响应压缩

减少网络传输数据量:

yaml 复制代码
server:
  compression:
    enabled: true
    mime-types: application/json,application/xml,text/html,text/plain
    min-response-size: 2048  # 超过 2KB 才压缩

Feign 请求压缩:

yaml 复制代码
spring:
  cloud:
    openfeign:
      compression:
        request:
          enabled: true
          mime-types: application/json
          min-request-size: 2048
        response:
          enabled: true

5.5 并行调用(CompletableFuture)

当需要调用多个独立服务时,串行改并行:

java 复制代码
// ❌ 串行调用:300ms + 200ms + 150ms = 650ms
Product product = productFeign.getProduct(productId);       // 300ms
Stock stock = stockFeign.getStock(productId);               // 200ms
List<Review> reviews = reviewFeign.getReviews(productId);   // 150ms

// ✅ 并行调用:max(300, 200, 150) = 300ms
CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync(
    () -> productFeign.getProduct(productId), asyncExecutor);
CompletableFuture<Stock> stockFuture = CompletableFuture.supplyAsync(
    () -> stockFeign.getStock(productId), asyncExecutor);
CompletableFuture<List<Review>> reviewFuture = CompletableFuture.supplyAsync(
    () -> reviewFeign.getReviews(productId), asyncExecutor);

// 等待所有完成
CompletableFuture.allOf(productFuture, stockFuture, reviewFuture).join();

// 获取结果
Product product = productFuture.get();
Stock stock = stockFuture.get();
List<Review> reviews = reviewFuture.get();

带超时控制:

java 复制代码
try {
  CompletableFuture.allOf(productFuture, stockFuture, reviewFuture)
      .get(5, TimeUnit.SECONDS);  // 最多等 5 秒
} catch (TimeoutException e) {
  log.warn("并行调用超时,使用已完成的结果");
  // 超时后取已完成的结果,未完成的用默认值
}

六、数据库连接池优化

6.1 HikariCP 参数详解

yaml 复制代码
spring:
  datasource:
    hikari:
      # 连接池大小
      minimum-idle: 10              # 最小空闲连接数
      maximum-pool-size: 50         # 最大连接数
      
      # 超时配置
      connection-timeout: 3000      # 获取连接超时(ms),获取不到抛异常
      idle-timeout: 600000          # 空闲连接存活时间(10分钟)
      max-lifetime: 1800000         # 连接最大生命周期(30分钟)
      
      # 健康检查
      connection-test-query: SELECT 1
      validation-timeout: 2000      # 验证连接超时
      
      # 其他
      auto-commit: true
      pool-name: OrderServicePool

6.2 连接池大小计算

经验公式:

复制代码
最大连接数 = (CPU核数 × 2) + 有效磁盘数

示例:4核 CPU + 1块 SSD
最大连接数 = 4 × 2 + 1 = 9(向上取整为 10)

实际生产中常用配置:

场景 minimum-idle maximum-pool-size
小型服务(低并发) 5 20
中型服务(中等并发) 10 50
大型服务(高并发) 20 100

关键原则:连接池不是越大越好。连接数过多会导致数据库 CPU 和内存压力增大,反而降低整体吞吐。


七、JVM 层面优化

7.1 常用 JVM 参数

bash 复制代码
java -jar my-service.jar \
  -Xms2g \                    # 初始堆大小
  -Xmx2g \                    # 最大堆大小(建议与 Xms 相同,避免动态调整)
  -XX:+UseG1GC \              # 使用 G1 垃圾回收器(JDK 17 默认)
  -XX:MaxGCPauseMillis=200 \  # 目标 GC 暂停时间
  -XX:+HeapDumpOnOutOfMemoryError \  # OOM 时自动 dump
  -XX:HeapDumpPath=/logs/heapdump.hprof

7.2 Tomcat 线程池

yaml 复制代码
server:
  tomcat:
    threads:
      min-spare: 20        # 最小工作线程数
      max: 400             # 最大工作线程数
    accept-count: 100      # 等待队列长度
    max-connections: 8192  # 最大连接数

八、监控与度量

8.1 关键性能指标

指标 健康标准 告警阈值
接口 P99 耗时 < 500ms > 2s
接口 QPS 按业务 接近上限 80%
数据库连接池使用率 < 70% > 90%
Redis 命中率 > 95% < 80%
JVM GC 频率 Young GC < 10次/分钟 Full GC > 0
线程池活跃线程 < 70% 最大值 > 90%

8.2 Spring Boot Actuator 端点

yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  endpoint:
    health:
      show-details: always
  metrics:
    tags:
      application: ${spring.application.name}

常用查询:

bash 复制代码
# 数据库连接池
GET /actuator/metrics/hikaricp.connections.active
GET /actuator/metrics/hikaricp.connections.pending

# HTTP 请求耗时
GET /actuator/metrics/http.server.requests?tag=uri:/api/order/create

# JVM 内存
GET /actuator/metrics/jvm.memory.used

# 线程池
GET /actuator/metrics/executor.active

九、性能优化检查清单

上线前检查

类别 检查项 状态
SQL 所有查询有分页或 LIMIT 限制
SQL WHERE 条件字段有索引
SQL 无 SELECT *,只查需要字段
SQL 无循环内单条查询(N+1)
缓存 热点数据有缓存
缓存 缓存有 TTL,不会无限增长
缓存 有缓存穿透防护(空值缓存)
并发 共享资源有锁保护
并发 非核心逻辑已异步化
并发 线程池有合理的拒绝策略
网络 HTTP 客户端使用连接池
网络 所有远程调用有超时设置
网络 独立服务间有批量接口
连接 数据库连接池大小合理
连接 Redis 连接池大小合理

十、总结

优化维度 核心手段 效果
查询优化 分页 + 索引 + 批量查询 减少 DB 负载,响应时间从秒级降到毫秒级
缓存优化 Redis + 本地缓存 + 多级缓存 减少 90%+ 的数据库访问
并发优化 异步 + 线程池 + 分布式锁 提高吞吐量,避免并发冲突
网络优化 连接池 + 批量调用 + 并行调用 减少网络往返,降低调用延迟

优化优先级:数据库(影响最大)→ 缓存(见效最快)→ 并发(提升吞吐)→ 网络(锦上添花)

核心原则:先测量,再优化。没有指标数据支撑的优化是盲目的。用 APM 工具(SkyWalking/Jaeger)定位瓶颈,针对性优化效果最好。

相关推荐
Flittly2 小时前
【AgentScope Java新手村系列】(9)SpringBoot集成
java·spring boot·spring
杨运交2 小时前
[033][缓存模块]基于 Redisson 的租户隔离 Redis Key 前缀设计
spring boot
vx-Biye_Design3 小时前
springboot安阳地区研学旅游服务小程序-计算机毕业设计源码12785
java·vue.js·windows·spring boot·tomcat·maven·mybatis
昇腾CANN3 小时前
【cann-samples系列】GroupedMatmul MX量化矩阵乘的深度性能优化实践
线性代数·性能优化·矩阵·昇腾·cann
隔壁阿布都3 小时前
ShedLock 分布式定时任务锁框架介绍
spring boot·分布式
地瓜伯伯4 小时前
从MESI缓存一致性协议讲透synchronized的底层
java·spring boot·spring·spring cloud·微服务·springcloud
zhenlai20124 小时前
Vue3 + SpringBoot + AI:我做了一个股票分析工具(第1周复盘)
人工智能·spring boot·后端
Devin~Y4 小时前
大厂 Java 面试实录:从音视频内容社区到 AI RAG 的全链路技术设计
java·spring boot·redis·spring cloud·微服务·kafka·音视频
承渊政道4 小时前
飞算JavaAI 智能引导背后的多 Agent 协作机制解析:从老旧 Java 后台升级到可运行工程
java·开发语言·spring boot·安全·intellij-idea·软件工程·ai编程