简介
在这篇文章中,我将向你展示使用Lightrun分析一个Java应用程序,这样你就可以发现各种性能调整的改进,你可以应用到你当前的Java应用程序。
在上一篇文章中,我解释了什么是Lightrun,以及你如何使用它来注入动态日志、捕获运行时快照或添加动态指标。
在这篇文章中,我将使用Lightrun作为我的JPA关联获取验证器的替代品。
DefaultLoadEventListener
当使用Hibernate获取JPA实体时,会触发一个 LoadEvent ,由 DefaultLoadEventListener ,处理方式如下。
DefaultLoadEventListener 将检查该实体是否位于当前JPA持久化上下文或第一级缓存中。如果在那里找到了实体,那么就会返回相同的对象引用。
这意味着,两个连续的实体获取调用将总是返回相同的Java Object 引用。这就是JPA和Hibernate提供应用级可重复读取的原因。
如果在第一级缓存中没有找到实体,Hibernate将尝试从第二级缓存中加载它,如果且仅当第二级缓存被启用。
最后,如果实体不能从任何缓存中加载,它将被从数据库中加载。
现在,这个过程可以在调用 EntityManager.find ,在遍历一个关联时发生,也可以间接地用于 FetchType.EAGER 策略。
检查N+1查询问题
JPA关联获取验证器文章解释了如何以编程方式断言JPA关联获取。这个工具在测试过程中非常有用,但是对于那些要第一次检查生产系统的顾问来说就不太实用了。
例如,让我们举一个 Spring PetClinic 应用程序的例子。
@Entity @Table(name = "pets") public class Pet extends NamedEntity { @Column(name = "birth_date") @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate birthDate; @ManyToOne @JoinColumn(name = "type_id") private PetType type; @ManyToOne @JoinColumn(name = "owner_id") private Owner owner; } 复制代码
Pet 实体有两个父关联, type 和 owner ,每个都被注解为 @ManyToOne 。然而,默认情况下, @ManyToOne 关联使用 FetchType.EAGER 的获取策略。
因此,如果我们加载2个 Pet 实体,同时也获取它们相关的 owner 关联。
List<Pet> pets = entityManager.createQuery(""" select p from Pet p join fetch p.owner where p.id in :petIds """) .setParameter("petIds", List.of(3L, 6L)) .getResultList(); 复制代码
Hibernate将执行3次查询。
SELECT p.id as id1_1_1_, p.name as name2_1_1_, p.birth_date as birth_da3_1_1_, p.owner_id as owner_id4_1_1_, p.type_id as type_id5_1_1_, o.id as id1_0_0_, o.first_name as first_na2_0_0_, o.last_name as last_nam3_0_0_, o.address as address4_0_0_, o.city as city5_0_0_, o.telephone as telephon6_0_0_ FROM pets p JOIN owners o ON o.id = p.owner_id WHERE p.id IN (3, 6) SELECT pt.id as id1_3_0_, pt.name as name2_3_0_ FROM types pt WHERE pt.id = 3 SELECT pt.id as id1_3_0_, pt.name as name2_3_0_ FROM types pt WHERE pt.id = 6 复制代码
那么,为什么会有3个查询被执行,而不是只有一个?这就是臭名昭著的N+1查询问题。
使用Lightrun进行Java性能调优
虽然你可以使用集成测试来检测N+1查询问题,但有时你不能这样做,因为你被雇来分析的系统已经部署到生产中,而你还没有看到源代码。
在这种情况下,像Lightrun这样的工具就变得非常方便,因为你可以简单地动态注入一个运行时快照,这个快照只有在满足特定条件时才会被记录。
第一步是在 DefaultLoadEventListener Hibernate类的 loadFromDatasource 方法中添加一个运行时快照。
注意,快照只记录相关的 LoadEvent 的 isAssociationFetch() 方法返回 true 。这个条件允许我们捕获N+1查询问题所执行的二次查询。
现在,当加载所有姓戴维斯的宠物主人时,PetClinic应用程序会执行以下SQL查询。
SELECT DISTINCT o.id AS id1_0_0_, p.id AS id1_1_1_, o.first_name AS first_na2_0_0_, o.last_name AS last_nam3_0_0_, o.address AS address4_0_0_, o.city AS city5_0_0_, o.telephone AS telephon6_0_0_, p.name AS name2_1_1_, p.birth_date AS birth_da3_1_1_, p.owner_id AS owner_id4_1_1_, p.type_id AS type_id5_1_1_, p.owner_id AS owner_id4_1_0__, p.id AS id1_1_0__ FROM owners o LEFT OUTER JOIN pets p ON o.id=p.owner_id WHERE o.last_name LIKE 'Davis%' SELECT pt.id as id1_3_0_, pt.name as name2_3_0_ FROM types pt WHERE pt.id = 6 SELECT pt.id as id1_3_0_, pt.name as name2_3_0_ FROM types pt WHERE pt.id = 3 复制代码
而在检查Lightrun快照控制台时,我们可以看到有两条记录被注册。
第一个快照看起来如下。
而第二个快照看起来是这样的。
请注意,由于大量使用了 FetchType.EAGER 策略,这两个快照对应于Spring Petclinic应用程序执行的二级查询。
很酷,对吗?
结论
虽然你可以在测试过程中使用JPA关联获取验证器来检测这些N+1查询问题,但如果你的任务是分析一个你从未见过的运行时系统,那么Lightrun就是一个发现各种问题及其发生原因的伟大工具。
特别是因为Java性能调优是我被雇用的最常见的原因之一,Lightrun是我的工具集的一个很好的补充。