У меня длинное (но довольно простое) приложение, использующее Hibernate (через JPA). По мере того, как он бежал, он наблюдал довольно резкое замедление. Я смог сузить до требуемого случайного вызова entityManager.clear()
. Когда диспетчер сущностей Hibernate отслеживает 100 000 объектов, он ~ 100 раз медленнее, чем когда он отслеживает только несколько (см. Результаты ниже). Мой вопрос: почему Hiberate так сильно замедляется, когда он отслеживает множество сущностей? И есть ли другие способы его решения?
!!! Обновление: я смог сузить это до кода автоматической сбрасывания Hibernate.!!!
В частности, метод org.hibernate.event.internal.AbstractFlushingEventListener
flushEntities()
(по крайней мере, в Hibernate 4.1.1.Final). В нем есть цикл, который выполняет итерации над объектами ВСЕ в контексте персистентности, выполняя некоторые обширные проверки вокруг каждого из них (хотя все объекты уже очищены в моем примере!).
Таким образом, частично отвечая на вторую часть моего вопроса, проблему производительности можно решить, установив режим сброса на FlushModeType.COMMIT
в запросе (см. обновленные результаты ниже). например.
Place place = em.createQuery("from Place where name = :name", Place.class)
.setParameter("name", name)
.setFlushMode(FlushModeType.COMMIT) // <-- yay!
.getSingleResult();
... но это похоже на довольно уродливое решение - передача ответственности за то, что вещи были очищены от методов запроса, а не в методах обновления. Это также в значительной степени означает, что я либо должен установить режим флеша для COMMIT во всех методах запросов, либо, скорее, установить его в EntityManager.
Это заставляет меня задуматься: это ожидаемое поведение? Я делаю что-то не так с покраснением или как я определяю сущности? Или это ограничение (или, возможно, ошибка) в спящем режиме?
Ниже приведен пример кода, который я использовал для изоляции проблемы:
Объект проверки
@Entity @Table(name="place") @Immutable
public class Place {
private Long _id;
private String _name;
@Id @GeneratedValue
public Long getId() { return _id; }
public void setId(Long id) { _id = id; }
@Basic(optional=false) @Column(name="name", length=700,
updatable=false, nullable=false, unique=true,
columnDefinition="varchar(700) character set 'ascii' not null")
public String getName() { return _name; }
public void setName(String name) { _name = name; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { return getName().hashCode(); }
}
Контрольный код
В тестовом коде я генерирую 100000 имен случайных мест и вставляю их. Затем запрашивает 5000 из них случайным образом по имени. В столбце имен есть индекс.
Place place = em.createQuery(
"select p from Place p where p.name = :name", Place.class)
.setParameter("name", name)
.getSingleResult();
Для сравнения и чтобы убедиться, что это не что-то в базе данных, я выполнил следующий запрос на основе JDBC (под em.unwrap(Session.class).doWork(...)
) по отдельным случайным образом выбранным именам на 5000 мест:
PreparedStatement ps = c.prepareStatement(
"select id, name from place where name = ?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Place place = new Place();
place.setId(rs.getLong(1));
place.setName(rs.getString(2));
}
rs.close();
ps.close();
(Обратите внимание: я создаю и закрываю PreparedStatement для каждого из 5000 запросов для эталона).
Результаты
Все приведенные ниже результаты усредняются по 5000 запросам. JVM было дано -Xmx1G
Seconds/Query Approach
0.000160s JDBC
0.000286s Hibernate calling clear() after import and every 100 queries
0.000653s Hibernate calling clear() once after the import
0.012533s Hibernate w/o calling clear() at all
0.000292s Hibernate w/o calling clear(), and with flush-mode COMMIT
Другие наблюдения: во время запросов на спящий режим (без каких-либо ясных вызовов), java-процесс привязал ядро при почти 100% использовании. JVM никогда не превышала 500 Мбайт кучи. Во время запросов также было много активности GC, но на загрузке процессора явно доминировал код Hibernate.