SpringBoot开发实战:从入门到精通

56 阅读3分钟

为什么你的SpringBoot应用在流量突增时突然OOM?

去年参与的一个电商大促项目,压测时一切正常,但凌晨流量峰值到来后,服务突然OOM崩溃。日志里堆内存耗尽,但GC日志显示老年代几乎空着——这不对劲。你一定也遇到过类似场景:为什么堆明明没满,却抛出OOM?

现象与根因:Metaspace的隐形杀手

问题出在JVM的Metaspace(元空间)。SpringBoot应用启动时大量加载类信息,而Metaspace默认只根据初始值动态扩展(-XX:MetaspaceSize),但不会自动释放。我们的服务动态生成代理类(比如Spring AOP、MyBatis Mapper),在流量激增时触发了高频类加载,最终Metaspace被撑爆。

关键证据:

# 错误日志
java.lang.OutOfMemoryError: Metaspace  
# JVM参数(错误示范)
- XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m

解决方案:不只是调大参数

单纯增大MaxMetaspaceSize只是拖延时间。正确的做法是限制动态类生成+合理回收策略:

  1. 给Metaspace预留缓冲:初始值(MetaspaceSize)应接近预期峰值,避免频繁扩容
  2. 强制卸载无用类(JDK8+):
// 启动参数(正确配置示例)
- XX:MetaspaceSize=256m
- XX:MaxMetaspaceSize=512m
- XX:+UseConcMarkSweepGC // 触发CMS的类卸载
- XX:+CMSClassUnloadingEnabled

压测对比:

配置方案稳定支持QPSMetaspace峰值
默认参数1.2k持续增长至OOM
调大不回收2.5k稳定在300MB
正确回收策略3.8k+240MB~260MB波动

避坑清单:Metaspace的3个高危场景

  1. 动态代理泛滥:Spring AOP、Feign客户端、MyBatis Mapper接口过多时,每个请求可能生成新代理类
  2. 热部署陷阱:DevTools频繁重启时,旧类未卸载导致Metaspace泄漏
  3. 默认参数误导:-XX:MetaspaceSize的实际含义是首次扩容阈值,不是初始分配大小

你以为的"轻量级"事务可能拖垮数据库

有一次排查慢SQL,发现一个@Transactional注解的方法在百万级数据表上执行了全表扫描。明明只是更新单条记录,为什么锁了整张表?如果你用的是默认事务隔离级别,这个坑可能正在你的代码里潜伏。

根因:Spring的默认传播行为与MySQL的间隙锁

Spring的@Transactional默认传播级别是REQUIRED,而MySQL InnoDB默认隔离级别是REPEATABLE-READ。当你的方法内部有查询+更新操作时:

// 错误写法:全表被间隙锁
@Transactional 
public void updateUserStatus(Long userId) {
    User user = userRepository.findByName("非索引字段"); // 全表扫描
    userRepository.updateStatus(user.getId(), "INACTIVE"); // 触发表级间隙锁
}
  • 关键机制*:
  • REPEATABLE-READ下,MySQL会对扫描过的所有索引范围加间隙锁
  • 非索引字段查询导致全表扫描 → 锁全表

正确姿势:索引+锁优化

  1. 强制走索引:
ALTER TABLE users ADD INDEX idx_name (name);
  1. 缩小事务范围:
// 正确写法:先按索引查ID,再短事务更新
public void updateUserStatusOpt(Long userId) {
    Long id = userRepository.findIdByName("name"); // 命中索引
    transactionalTemplate.execute(status -> {
        userRepository.updateStatus(id, "INACTIVE");
        return null;
    });
}

实测效果(10万行users表):

方案平均耗时锁等待次数
全表扫描事务1200ms85
索引+短事务28ms0

事务优化的3条军规

  1. 永远避免在事务内做非索引查询
  2. 读多写少场景考虑@Transactional(readOnly=true)
  3. 批量操作使用PROPAGATION_REQUIRES_NEW分隔

SpringBoot的"约定优于配置"是把双刃剑——它用默认设置帮你快速启动,但也把真正的技术债务藏在细节里。我的经验是:永远不要直接使用任何未经验证的默认值。

你在项目中有没有遇到过其他"默认设定"导致的性能问题?欢迎分享你的实战案例。