⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 xxy开源 「xxy开源」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

在 基于 Spring 全家桶 开发的应用中,本地开发时一切正常,接口响应也很快。 可一旦上线、并发一上来,系统却突然变慢,数据库压力暴涨,却又查不出明显的慢 SQL。

如果你遇到过这种情况,很可能正在无意中踩中 Spring Boot 在 JPA 场景下默认开启的一个“性能陷阱”:Open Session in View(OSIV)。

它不会报错、不影响功能,却能在你毫无察觉的情况下,悄悄拖垮系统性能。

Open Session in View 是一种设计模式,现在更多时候被认为是一种反模式。

它的核心行为可以用一句话概括:

在整个 HTTP 请求生命周期内,保持 Hibernate Session 处于打开状态。

也就是说:

  • 事务已经结束
  • Service 方法已经返回
  • 但 Persistence Context 仍然存活
  • JSON 序列化、视图渲染阶段,依然可以触发数据库查询

注意:

OSIV 不会一直占用数据库连接, 但它允许 在任何不可预测的时刻执行 SQL

这才是真正的问题所在。

OSIV 最初是为 传统服务端渲染页面 设计的:

  • JSP
  • Thymeleaf
  • 在 View 层直接访问懒加载关联对象

如果不保持 Session 打开,就会抛出 LazyInitializationException。

在那个时代,这是一种“方便”的解决方案。

但在今天的 REST API + JSON 序列化 架构下,这种便利,代价却极其高昂。

很多人第一次接触 OSIV,都是在 Spring Boot 的启动日志里看到类似这样的一条警告:

WARN: spring.jpa.open-in-view is enabled by default. 
Therefore, database queries may be performed during view rendering.
Explicitly configure spring.jpa.open-in-view to disable this warning

问题在于:

  • 日志只是 WARN
  • 应用照常运行
  • 接口返回完全正常

于是大多数团队都会选择忽略它。

既然 Spring Boot 默认开着,那应该没问题吧?

这正是 OSIV 最危险的地方。

假设你有一个典型的电商领域模型:

Customer (1) ── (n) PurchaseOrder (1) ── (n) OrderItem (n) ── (1) Product

JPA 实体(关键部分)如下:

@Entity
public class Customer {

@OneToMany(mappedBy = "customer")
private List<PurchaseOrder> orders = new ArrayList<>();

}

@Entity
public class PurchaseOrder {

@ManyToOne(cascade = CascadeType.ALL, optional = false)
private Customer customer;

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "purchase_order_id")
private List<OrderItem> items = new ArrayList<>();

}

@Entity
public class OrderItem {

@ManyToOne(fetch = FetchType.LAZY)
private Product product;

}

REST 接口:

@GetMapping("/osiv")
List<PurchaseOrderDTO> getPurchaseOrders() {
var orders = purchaseOrderRepository.findAll();
return orders.stream()
.map(o -> modelMapper.map(o, PurchaseOrderDTO.class))
.toList();
}

Repository 类:

public interface PurchaseOrderRepository
extends JpaRepository<PurchaseOrder, Long> {
}

一切看起来都很正常,对吧?

当请求 /orders/osiv 时,真实的执行过程是:

  1. findAll():1 条 SQL,查订单

  2. DTO 映射开始:ModelMapper 遍历对象图

  3. 触发懒加载:

    • order.customer → N 条 SQL
  • order.items → N 条 SQL
  • item.product → N × M 条 SQL

假设:

  • 100 个订单
  • 每个订单 5 个商品

最终 SQL 数量是:

1(订单)
+ 100(客户)
+ 100(订单项)
+ 500(商品)
= 701 条 SQL

一个接口,700 多次数据库访问。

如果关闭 OSIV:

  • 事务结束后再访问懒加载属性会 立即抛出 LazyInitializationException
  • 问题暴露得非常早

如果开启 OSIV(默认):

  • Session 一直开着
  • 懒加载“正常工作”
  • 接口返回正确结果
  • 性能问题被完全掩盖

直到某一天:

  • 并发升高
  • 数据量变大
  • 数据库 CPU 飙升

然后问题才在生产环境“爆炸”。

正确的使用方式应该是 显式加载你需要的数据:

@Query("""
select o from PurchaseOrder o
join fetch o.items i
join fetch o.customer
join fetch i.product
""")
List<PurchaseOrder> findAllFetchRelations();

最终只会执行 一条 SQL:

SELECT o.*, i.*, c.*, p.*
FROM purchase_order o
JOIN order_item i ON ...
JOIN customer c ON ...
JOIN product p ON ...

然后,关掉 OSIV:

spring.jpa.open-in-view=false

OSIV 解决的是“表面问题”,却 隐藏了真正的架构缺陷。

在现代 REST API 架构下,性能和可控性,必须优先于 OSIV 带来的便利。 Spring Boot 默认开启 OSIV,只是为了降低上手门槛,而不是最佳实践。

参考链接:

文章目录