Рубрики
Без рубрики

Почему вы никогда не должны использовать генератор идентификаторов ТАБЛИЦ с JPA и гибернацией

Автор оригинала: Vlad Mihalcea.

С точки зрения доступа к данным JPA поддерживает два основных типа идентификаторов:

  • назначенный
  • сгенерированный

Назначенные идентификаторы должны быть установлены вручную для каждого данного объекта перед сохранением . По этой причине назначенные идентификаторы подходят для естественных ключей.

Для синтетических первичных ключей нам необходимо использовать сгенерированный идентификатор сущности , который поддерживается JPA с помощью аннотации @GeneratedValue .

Существует четыре типа стратегий сгенерированных идентификаторов, которые определяются типом Генерации перечисления:

  • АВТО
  • ИДЕНТИЧНОСТЬ
  • ПОСЛЕДОВАТЕЛЬНОСТЬ
  • СТОЛ

Стратегия АВТО генератор идентификаторов выбирает одну из трех других стратегий ( ИДЕНТИФИКАЦИЯ , ПОСЛЕДОВАТЕЛЬНОСТЬ или ТАБЛИЦА ) на основе базовых возможностей реляционной базы данных.

В то время как ИДЕНТИФИКАТОР сопоставляется с автоматически увеличиваемым столбцом (например, ИДЕНТИФИКАТОР в SQL Server или AUTO_INCREMENT в MySQL) и ПОСЛЕДОВАТЕЛЬНОСТЬ используется для делегирования генерации идентификатора последовательности базы данных, генератор ТАБЛИЦЫ не имеет прямой реализации в реляционных базах данных.

В этом посте будет проанализировано, почему генератор ТАБЛИЦЫ является плохим выбором для каждого корпоративного приложения, которое заботится о производительности и масштабируемости.

Чтобы понять, как работает генератор ТАБЛИЦЫ , рассмотрим следующее сопоставление Post сущностей:

@Entity 
@Table(name = "post")
public class Post {

    @Id
    @GeneratedValue(strategy=GenerationType.TABLE)
    private Long id;
}    

Следующий вывод получается при вставке новой Записи сущности:

SELECT tbl.next_val 
FROM hibernate_sequences tbl 
WHERE tbl.sequence_name=default 
FOR UPDATE

INSERT INTO hibernate_sequences (sequence_name, next_val) 
VALUES (default, 1)

UPDATE hibernate_sequences SET next_val=2 
WHERE next_val=1 AND sequence_name=default

SELECT tbl.next_val 
FROM hibernate_sequences tbl 
WHERE tbl.sequence_name=default 
FOR UPDATE

UPDATE hibernate_sequences SET next_val=3  
WHERE next_val=2 AND sequence_name=default

DEBUG - Flush is triggered at commit-time

INSERT INTO post (id) values (1, 2)

Генератор таблиц извлекает выгоду из пакетной обработки JDBC, но каждое обновление последовательности таблиц выполняется в три этапа:

  • Инструкция блокировки выполняется для обеспечения того, чтобы одно и то же значение последовательности не выделялось для двух одновременных транзакций.
  • Текущее значение увеличивается на уровне доступа к данным.
  • Новое значение сохраняется обратно в базу данных, и вторичная транзакция фиксируется таким образом, чтобы снять блокировку на уровне строк.

В отличие от столбцов и последовательностей идентификаторов, которые могут увеличивать последовательность за один запрос, генератор ТАБЛИЦЫ влечет за собой значительные затраты на производительность. По этой причине Hibernate поставляется с серией оптимизаторов, которые могут повысить производительность как для ПОСЛЕДОВАТЕЛЬНОСТИ , так и для ТАБЛИЦЫ генераторов, таких как объединенные или объединенные в пул оптимизаторы .

Хотя это стратегия генерации переносимых идентификаторов, генератор ТАБЛИЦЫ вводит сериализуемое выполнение (блокировку на уровне строк), что может препятствовать масштабируемости.

По сравнению с этим методом генерации последовательностей на уровне приложений столбцы и последовательности идентификаторов высоко оптимизированы для сценариев с высоким уровнем параллелизма и должны быть предпочтительным выбором

Из-за несоответствия между генератором идентификаторов и транзакционным кэшем с последующей записью JPA предлагает альтернативный генератор, подобный последовательности, который работает даже в тех случаях, когда последовательности изначально не поддерживаются.

Таблица базы данных используется для хранения последнего значения последовательности, а блокировка на уровне строк используется для предотвращения получения двумя параллельными соединениями одного и того же значения идентификатора.

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

Последовательности баз данных используют выделенные блокировки, чтобы предотвратить получение одновременными транзакциями одного и того же значения, но блокировки снимаются, как только счетчик увеличивается. Такая конструкция обеспечивает минимальную конкуренцию, даже если последовательность используется одновременно несколькими параллельными транзакциями. Использование таблицы базы данных в качестве последовательности является сложной задачей, так как для предотвращения получения двумя транзакциями одного и того же значения последовательности необходимо использовать блокировку на уровне строк. Однако, в отличие от блокировок объектов последовательности, блокировка на уровне строк является транзакционной, и после ее получения она может быть освобождена только после завершения текущей транзакции (либо фиксации, либо отката).

Это было бы ужасной проблемой масштабируемости, потому что длительная транзакция помешала бы любой другой транзакции получить новое значение последовательности. Чтобы справиться с этим ограничением, для получения нового значения последовательности используется отдельная транзакция базы данных. Таким образом, блокировка на уровне строк, связанная с увеличением значения счетчика последовательностей, может быть снята, как только транзакция обновления последовательности завершится.

Для локальных транзакций (например, RESOURCE_LOCAL в терминологии JPA) новая транзакция означает получение другого соединения с базой данных и его фиксацию после выполнения логики обработки последовательности. Это может оказать дополнительное давление на базовый пул соединений , особенно если уже существует значительная конкуренция за подключения к базе данных.

В среде JTA текущая транзакция должна быть приостановлена, а значение последовательности извлекается в отдельной транзакции. Менеджеру транзакций JTA приходится выполнять дополнительную работу по переключению контекста транзакций, что также может повлиять на общую производительность приложения.

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

Чтобы оценить стоимость параллелизма для каждого генератора идентификаторов, следующий тест измеряет время, необходимое для вставки 100 Post сущностей, когда задействовано несколько запущенных потоков.

Пакетирование JDBC включено, и пул соединений настроен таким образом, чтобы обеспечить максимальное количество требуемых подключений к базе данных (например, 32). На самом деле приложение может быть не настроено с таким количеством подключений к базе данных, и стоимость подключения ТАБЛИЦЫ генератора может быть еще выше.

Первая тестируемая система реляционных баз данных поддерживает столбцы идентификаторов, поэтому стоит измерить, как конкурируют идентификатор и ТАБЛИЦА генератор, поскольку генератор Hibernate IDENTITY не поддерживает пакетирование JDBC для операторов INSERT, как описано в этой статье . Каждая итерация теста увеличивает конкуренцию, выделяя больше рабочих потоков, которым необходимо выполнить одну и ту же загрузку вставки базы данных.

Даже если он не может извлечь выгоду из пакетной обработки JDBC, генератору IDENTITY все равно удается превзойти генератор TABLE , который использует объединенный оптимизатор с шагом 100.

Чем больше потоков используется, тем менее эффективным становится генератор таблиц. С другой стороны, столбцы идентификаторов масштабируются намного лучше при большем количестве одновременных транзакций. Даже если не поддерживает пакетирование JDBC, собственные столбцы идентификаторов по-прежнему являются допустимым выбором, и в будущем Hibernate может даже поддерживать пакетные вставки для них.

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

При выполнении того же теста в реляционной базе данных, поддерживающей последовательности, записываются следующие результаты:

Влияние генератора ТАБЛИЦЫ на производительность становится заметным в сильно параллельных средах, где блокировка на уровне строк и переключатель подключения к базе данных обеспечивают последовательное выполнение.

Если вам понравилась эта статья, держу пари, вам также понравятся моя Книга и Видеокурсы.

Решение блокировки на уровне строк, используемое генератором ТАБЛИЦЫ , содержит часть сериализации, которая препятствует параллелизму, как объясняется Законом Универсальной масштабируемости (который является обобщением закона Амдала ).

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