Подтвердить что ты не робот

Спящий режим запрашивает гораздо медленнее с flushMode = AUTO, пока не будет вызван clear()

У меня длинное (но довольно простое) приложение, использующее 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.

4b9b3361

Ответ 1

Но в основном мне любопытно, почему Hibernate, по-видимому, демонстрирует O (n) или даже O (n ^ 2) поиски запросов - похоже, он должен иметь возможность использовать хеш-таблицу или двоичное дерево под капотом, чтобы быстро сохраняйте запросы. Обратите внимание на разницу в 2 порядка по величине при отслеживании 100000 объектов против 100 объектов.

Сложность O (n²) возникает из-за того, как нужно обрабатывать запрос. Поскольку Hibernate внутренне откладывает обновления и вставляет до тех пор, пока это возможно (чтобы использовать возможность группировать подобные обновления/вставки вместе, особенно если вы задаете несколько свойств объекта).

Итак, прежде чем вы сможете настраивать объекты запроса в базе данных, Hibernate должен обнаруживать все изменения объектов и очищать все изменения. Проблема здесь в том, что в спящем режиме также происходит некоторое уведомление и перехват. Таким образом, он выполняет итерацию по каждому объекту объекта, управляемому контекстом персистентности. Даже если объект сам по себе не изменен, он может содержать изменяемые объекты или даже контрольные коллекции.

Кроме того, механизм перехвата позволяет вам получить доступ к любому объекту, который считается грязным, чтобы ваш собственный код мог выполнять дополнительные проверки загрязнения или выполнять дополнительные вычисления, такие как вычисление сумм, средних значений, запись дополнительной информации и т.д.

Но давайте посмотрим на код в течение минуты:

Вызов флеша для подготовки результатов запроса:

DefaultFlushEventListener.onFlush(..)

- > AbstractFlushingEventListener.flushEverythingToExecution(event)   - > AbstractFlushingEventListener.prepareEntityFlushes(..)

Реализация использует:

for ( Map.Entry me : IdentityMap.concurrentEntries( persistenceContext.getEntityEntries() ) ) {
        EntityEntry entry = (EntityEntry) me.getValue();
        Status status = entry.getStatus();
        if ( status == Status.MANAGED || status == Status.SAVING || status == Status.READ_ONLY ) {
            cascadeOnFlush( session, entry.getPersister(), me.getKey(), anything );
        }
    }

Как вы можете видеть, карта всех объектов в контексте персистентности извлекается и повторяется.

Это означает, что для каждого вызова запроса вы перебираете все прежние результаты, чтобы проверить наличие грязных объектов. И еще больше cascadeOnFlush создает новый объект и делает еще больше вещей. Вот код cascadeOnFlush:

private void cascadeOnFlush(EventSource session, EntityPersister persister, Object object, Object anything)
throws HibernateException {
    session.getPersistenceContext().incrementCascadeLevel();
    try {
        new Cascade( getCascadingAction(), Cascade.BEFORE_FLUSH, session )
        .cascade( persister, object, anything );
    }
    finally {
        session.getPersistenceContext().decrementCascadeLevel();
    }
}

Итак, это объяснение. Hibernate проверяет каждый объект, управляемый контекстом персистентности, каждый раз, когда вы выдаете запрос.

Итак, для всех, кто это читает, это вычисление сложности: 1. Запрос: 0 объектов 2. Запрос: 1 объект 3. Запрос: 2 объекта .. 100. Запрос: 100 объектов , .. 100k + 1 Запрос: 100k записей

Итак, мы имеем O (0 + 1 + 2... + n) = O (n (n + 1)/2) = O (n²).

Это объясняет ваше наблюдение. Для поддержания небольшого cpu и памяти спящий режим управляемый постоянный контекст должен быть как можно меньше. Если позволить Hibernate управлять больше, чем позволяет сказать, что 100 или 1000 сущностей значительно замедляют спящий режим. Здесь следует рассмотреть возможность изменения режима флеша, использовать второй сеанс для запроса и один для изменения (если это вообще возможно) или использовать StatelessSession.

Итак, ваше наблюдение правильное, происходит O (n²).

Ответ 2

Возможно, вам известно, что EntityManager отслеживает постоянные объекты (т.е. созданные с помощью вызова em.createQuery(...).getSingleResult()). Они накапливаются в так называемом постоянном контексте или сеансе (термин Hibernate) и позволяют использовать очень опрятные. Например, вы можете изменить объект, вызвав метод mutator setName(...), а EntityManager будет синхронизировать это изменение состояния в памяти с базой данных (выдаст инструкцию UPDATE) всякий раз, когда это уместно. Это происходит, не требуя вызова явных методов save() или update(). Все, что вам нужно, - это работать с объектом, как если бы это был нормальный объект Java, а EntityManager позаботится о сохранении.

Почему это медленно (er)?

Во-первых, он обеспечивает наличие только одного, единственного экземпляра для первичного ключа в памяти. Это означает, что если вы загружаете одну и ту же строку дважды, в куче будет создан только один объект (оба результата будут ==). Это имеет большой смысл - представьте, если у вас есть 2 копии одной и той же строки, EntityManager не может гарантировать, что он надежно синхронизировал объект Java, так как вы можете вносить изменения в оба объекта самостоятельно. Возможно, есть много других операций низкого уровня, которые в конечном итоге замедляют EntityManager, если есть много объектов, которые нужно отслеживать. Методы clear() фактически удаляют там объекты в постоянный контекст и облегчают задачу (меньше объектов для отслеживания = более быстрая операция).

Как вы можете обойти это?

Если ваша реализация EntityManager - это Hibernate, вы можете использовать StatelessSession, которая предназначена для устранения этих штрафов за производительность. Я думаю, вы можете получить его через:

StatelessSession session = ((Session) entityManager.getDelegate()).getSessionFactory().openStatelessSession();

(код NB! не проверен, взятый из другого вопроса )