JPA持续变得越来越慢
此场景在两个方向上使用简单的oneToMany关系与级联持续存在。JPA持续变得越来越慢
很多:
@javax.persistence.Entity(name="Many")
public class Many {
@javax.persistence.ManyToOne(cascade = CascadeType.PERSIST)
protected One one;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long primaryKey;
public void setM(One one) {
this.one = one;
// comment out this line and performance becomes stable
this.one.getMany().add(this);
}
// other setters, getters, etc...
}
一:
@javax.persistence.Entity(name="One")
public class One {
@javax.persistence.OneToMany(mappedBy="m", cascade = CascadeType.PERSIST)
protected java.util.Set<Many> many = com.google.common.collect.Sets.newHashSet();
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long primaryKey;
private String name;
// setters, getters, etc...
}
测试:
public static void main(String[] args) {
while(true) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("test-pu");
EntityManager em = emf.createEntityManager();
for (int i = 0; i < 100; i++) {
sw.reset();
sw.start();
persistMVs(emf, em);
System.err.println("Elapsed: " + sw.elapsed(TimeUnit.MILLISECONDS) + " ms");
}
em.close();
emf.close();
}
}
private static void persistMVs(EntityManagerFactory emf, EntityManager em) {
em.getTransaction().begin();
One one = getOrCreateOne(em);
for (int i = 0; i < 200; i++) {
Many many = new Many();
many.setM(one);
em.persist(many);
}
em.getTransaction().commit();
}
测试是一个无限循环,其尝试插入与单个One
实体关联的20000个Many
个实体。每个循环从创建新的EntityManagerFactory
开始,以显示增加的数据库的负面性能影响。
预期的行为是,实体的插入时间不会急剧增加,但是在每个WHILE CYCLE之后会有一个数量级的增加。
注:
- 我已经试过的EclipseLink,休眠,OpenJPA的和所有从这种放缓的遭遇。
- 如果我不更新One的许多集合,那么就没有降级(请参阅Many的注释行)。
- 如果我没有创建一个新的EntityManagerFactory,那么即使在五十万个实体之后也不会退化。
- 缓慢的部分是
em.persist(many);
(我测量了它)。 - 检出https://github.com/kupsef/OneToMany并使用
gradle start
开始测试。
为什么在这种情况下数据库的初始大小很重要?我应该将此行为视为错误吗?
为了扩展Predrag的答案 - 遍历1:M关系不仅带来实体和任何扩展对象图的成本,而且这些实体仍然在持久单元中进行管理。由于您的测试重复使用同一个EntityManager进行重复事务,因此每次迭代都会继续增加被管实体的缓存。每次上下文与数据库同步时,都必须遍历该受管实体高速缓存并检查更改 - 这发生在刷新,事务提交或甚至查询上。
如果您必须引入大对象图表,可以采取哪些措施来缓解这种情况,可以为每个事务边界释放并获取新的EntityManagers,或者偶尔清空并清除EntityManager。任何一个选项都允许它释放一些被管实体,所以它不需要在每次提交时检查它们全部的变化。
编辑> 您的“Many”类重写了hashCode方法,并正在使用其主键引用的“One”的哈希码来构建其哈希码。这会导致循环中持久化的每个“Many”都具有相同的散列码,因为GenerationType.IDENTITY只能在插入语句发生时分配序列 - 在同步期间发生(flush/commit)。此方法可能会导致缓存查找,这种情况发生在提供程序由于级联持续调用而在每次持续呼叫中遍历不断增长的对象模型时花费的时间越来越长。
缓存的大小增长,我知道,但没有EMF的重新创建,性能没有下降,即使在这种情况下所有实体将被管理并存在于缓存中。还要注意循环的内部循环在while循环内占用相同的时间,但在每次迭代之后会大幅增加。 – kupsef
尝试删除关系中的级联持久选项,以便坚持不必通过不断增长的对象模型级联,但我不确定我是否理解了您的陈述。 EM拥有自己的缓存,并且每个传入的对象都必须进行检查。你也覆盖了equals和hashcode - 尝试删除你的实现,看看是否影响你的结果。 – Chris
事实上,问题是使用equals/hashCode,但不是您期望的方式:)没有它们,插入时间会在每个循环内部(在一个周期内保持不变)之后增加。所以严重执行的equals/hashCode不知何故隐藏了暂时的降级。 – kupsef
我认为问题出在this.one.getMany()
,因为在每次迭代中需要从这个关系中加载越来越多的实体。
@OneToMany
默认情况下关系是懒惰的,因此当您调用getMany()
时,JPA提供程序必须初始化集合中的每个实体,随着它的大小增长需要更多时间。
如果您在每次迭代中都没有创建新的EntityManagerFactory
,则上次迭代的实体将保留在缓存中,这样会执行更少的查询。
你为什么不看日志并理解它? –
你会建议什么日志? sql日志仅在第一个周期(内部for)有所不同,它还包含获取Many实体。这并不能解释随后的循环无法获取它们(这很可能是因为它们被缓存以备后用,正如所料)。 – kupsef
您使用的JPA实施的日志。我使用的实现(DataNucleus)总是显示很多信息来跟踪潜在的问题,所以我假定其他人同样有用 –