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) → 可命中 a、a,b、a,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:
javaRLock 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)定位瓶颈,针对性优化效果最好。