Автор оригинала: Vlad Mihalcea.
Вступление
Чтобы убедиться, что ваше приложение Spring Boot соответствует требованиям SLA (Соглашение об уровне обслуживания), вам необходим инструмент мониторинга производительности.
В этой статье я собираюсь показать вам, как вы можете отслеживать уровень доступа к данным приложения Spring Boot с помощью оптимизатора гиперсистенции .
Модель предметной области
Давайте предположим, что у нас есть приложение, которое использует следующее Магазин
и Сведения о магазине
организации:
Объект Store
сопоставляется с таблицей store
следующим образом:
@Entity @Table(name = "stores") public class Store { @Id private Integer id; private String name; @OneToOne( mappedBy = "store", cascade = CascadeType.ALL, optional = true ) private StoreDetails details; public Integer getId() { return id; } public Store setId(Integer id) { this.id = id; return this; } public String getName() { return name; } public Store setName(String name) { this.name = name; return this; } public StoreDetails getDetails() { return details; } public Store setDetails(StoreDetails details) { this.details = details; details.setStore(this); return this; } }
Обратите внимание, что методы настройки свойств используют API в стиле Fluent для упрощения процесса создания объектов.
Поскольку свойство details
отображает двунаправленную ассоциацию @OneToOne , метод setDetails
синхронизирует обе стороны ассоциации. Для получения более подробной информации о том, почему JPA и Hibernate требуют синхронизации обеих сторон двунаправленной связи, ознакомьтесь с этой статьей .
Сведения о магазине
дочерняя сущность отображается следующим образом:
@Entity @Table(name = "store_details") public class StoreDetails { @Id private Integer id; @Column(name = "created_on") private LocalDate createdOn = LocalDate.now(); private String owner; @OneToOne @MapsId @JoinColumn(name = "id") private Store store; public Integer getId() { return id; } public StoreDetails setId(Integer id) { this.id = id; return this; } public LocalDate getCreatedOn() { return createdOn; } public StoreDetails setCreatedOn(LocalDate createdOn) { this.createdOn = createdOn; return this; } public String getOwner() { return owner; } public StoreDetails setOwner(String owner) { this.owner = owner; return this; } public Store getStore() { return store; } public void setStore(Store store) { this.store = store; } }
Обратите внимание, что в отображении @OneToOne
используется аннотация @MapsId
, так как в противном случае отношение таблицы было бы не один к одному, а один ко многим.
Слой репозитория Spring Boot
Интерфейс Репозитория хранилища
расширяет данные Spring JpaRepository
и наш собственный Пользовательский репозиторий хранилища
интерфейс, подобный этому:
public interface StoreRepository extends JpaRepository, CustomStoreRepository { }
Репозиторий Пользовательское хранилище
определяет вставить все
метод:
public interface CustomStoreRepository { void insertAll(Iterablestores); }
Реализация метода insert All
выполняется в классе Custom Store RepositoryImpl
:
@Repository public class CustomStoreRepositoryImpl implements CustomStoreRepository { @PersistenceContext private EntityManager entityManager; @Override @Transactional public void insertAll(Iterablestores) { Session session = entityManager.unwrap(Session.class); session.setJdbcBatchSize(100); for (Store store : stores) { entityManager.persist(store); } } }
Метод insertAll
устанавливает размер пакета JDBC равным 100
а затем вызывает persist
метод EntityManager
для каждого Магазин
объект, который был предоставлен.
Уровень обслуживания весенней Загрузки
Уровень сервиса содержит Сервис магазина
интерфейс:
public interface StoreService { ListfindAll(); void saveAll(Iterable entities); void insertAll(Iterable entities); }
И класс StoreServiceImpl
:
@Service @Transactional(readOnly = true) public class StoreServiceImpl implements StoreService { @Autowired private StoreRepository storeRepository; @Override public ListfindAll() { return storeRepository.findAll(); } @Override @Transactional public void saveAll(Iterable stores) { storeRepository.saveAll(stores); } @Override @Transactional public void insertAll(Iterable stores) { storeRepository.insertAll(stores); } }
Обратите внимание, что по умолчанию методы службы используют транзакционный контекст только для чтения, чтобы воспользоваться преимуществами оптимизации гибернации только для чтения . Методы, которым необходимо вставлять, обновлять или удалять записи, должны вместо этого использовать транзакцию чтения-записи.
Настройка Spring Boot для использования оптимизатора сохраняемости Hy
Для мониторинга производительности мы будем использовать оптимизатор сохраняемости Hy, который можно настроить следующим образом:
@Configuration public class HypersistenceConfiguration { @Bean public HypersistenceOptimizer hypersistenceOptimizer( EntityManagerFactory entityManagerFactory) { return new HypersistenceOptimizer( new JpaConfig( entityManagerFactory ) .setProperties( Map.of( Config.Property.Session.TIMEOUT_MILLIS, 1000, Config.Property.Session.FLUSH_TIMEOUT_MILLIS, 500 ) ) ); } }
Свойство TIMEOUT_MILLIS
указывает, что контекст сохранения не должен занимать более 1000 миллисекунд, а свойство FLUSH_TIMEOUT_MILLIS
определяет максимально допустимый порог в 500 миллисекунд для очистки контекста сохранения.
Мониторинг производительности Spring Boot при сохранении 500 объектов
Чтобы убедиться, что требования SLA выполнены, мы собираемся выполнить метод тестового случая, который вставляет заданное количество Хранилища
и Сведения о магазине
организации:
private ListnewStores(int storeCount) { List stores = new ArrayList<>(); for (int i = 1; i <= storeCount; i++) { stores.add( new Store() .setId(i) .setName(String.format("Store no %d", i)) .setDetails( new StoreDetails() .setId(i) .setOwner("Vlad Mihalcea") ) ); } return stores; }
Теперь при создании 500 Магазинов
и Детали магазина
:
hypersistenceOptimizer.getEvents().clear(); storeService.saveAll(newStores(500)); assertTrue(hypersistenceOptimizer.getEvents().isEmpty());
Оптимизатор сохраняемости Hy уведомляет нас о том, что контекст сохранения выполняется более секунды:
Hypersistence Optimizer: CRITICAL - SessionTimeoutEvent - The JPA EntityManager or Hibernate Session has run for [1230] ms. You should avoid long-running Persistence Contexts as they can impact both the user experience and resource usage. For more info about this event, check out this User Guide link -> https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#SessionTimeoutEvent
При просмотре журнала приложений мы видим, что были выполнены следующие инструкции SQL:
select store0_.id as id1_4_1_, store0_.name as name2_4_1_, storedetai1_.id as id1_3_0_, storedetai1_.created_on as created_2_3_0_, storedetai1_.owner as owner3_3_0_ from stores store0_ left outer join store_details storedetai1_ on store0_.id=storedetai1_.id where store0_.id=? select storedetai0_.id as id1_3_0_, storedetai0_.created_on as created_2_3_0_, storedetai0_.owner as owner3_3_0_ from store_details storedetai0_ where storedetai0_.id=? select store0_.id as id1_4_1_, store0_.name as name2_4_1_, storedetai1_.id as id1_3_0_, storedetai1_.created_on as created_2_3_0_, storedetai1_.owner as owner3_3_0_ from stores store0_ left outer join store_details storedetai1_ on store0_.id=storedetai1_.id where store0_.id=? select storedetai0_.id as id1_3_0_, storedetai0_.created_on as created_2_3_0_, storedetai0_.owner as owner3_3_0_ from store_details storedetai0_ where storedetai0_.id=? -- 497 pairs of SQL queries deleted for brevity select store0_.id as id1_4_1_, store0_.name as name2_4_1_, storedetai1_.id as id1_3_0_, storedetai1_.created_on as created_2_3_0_, storedetai1_.owner as owner3_3_0_ from stores store0_ left outer join store_details storedetai1_ on store0_.id=storedetai1_.id where store0_.id=? select storedetai0_.id as id1_3_0_, storedetai0_.created_on as created_2_3_0_, storedetai0_.owner as owner3_3_0_ from store_details storedetai0_ where storedetai0_.id=? insert into stores (name, id) values (?, ?) insert into store_details (created_on, owner, id) values (?, ?, ?) insert into stores (name, id) values (?, ?) insert into store_details (created_on, owner, id) values (?, ?, ?) -- 497 pairs of SQL queries deleted for brevity insert into stores (name, id) values (?, ?) insert into store_details (created_on, owner, id) values (?, ?, ?)
Как объяснено в этой статье , запросы SELECT
выполняются, поскольку saveAll
метод JpaRepository
использует слияние
вместо сохраняется
, когда сущность использует присвоенный идентификатор.
Кроме того, пакетирование JDBC не используется, поэтому запуск этого метода занял более секунды.
Мониторинг производительности пружинной загрузки – результаты оптимизации
Прежде всего, мы собираемся добавить следующие свойства конфигурации:
spring.jpa.properties.hibernate.jdbc.batch_size=5 spring.jpa.properties.hibernate.order_inserts=true spring.jpa.properties.hibernate.order_updates=true
Как объяснено в этой статье , нам нужно установить размер пакета JDBC и включить порядок инструкций INSERT и UPDATE, чтобы максимально использовать механизм автоматического дозирования, используемый Hibernate.
Теперь вместо использования по умолчанию Сохранить все
метод JpaRepository
, , мы будем использовать метод
вставить все , который мы определили в
Хранилище пользовательских хранилищ
hypersistenceOptimizer.getEvents().clear(); storeService.saveAll(newStores(500)); assertTrue(hypersistenceOptimizer.getEvents().isEmpty());
И тест проходит, так как оптимизатор сохраняемости Hy не генерирует никаких событий.
Если мы проверим журнал приложений, то увидим, что пакетирование действительно используется:
insert into stores (name, id) values (?, ?) o.h.e.jdbc.batch.internal.BatchingBatch : Executing batch size: 100 o.h.e.jdbc.batch.internal.BatchingBatch : Executing batch size: 100 o.h.e.jdbc.batch.internal.BatchingBatch : Executing batch size: 100 o.h.e.jdbc.batch.internal.BatchingBatch : Executing batch size: 100 o.h.e.jdbc.batch.internal.BatchingBatch : Executing batch size: 100 insert into store_details (created_on, owner, id) values (?, ?, ?) o.h.e.jdbc.batch.internal.BatchingBatch : Executing batch size: 100 o.h.e.jdbc.batch.internal.BatchingBatch : Executing batch size: 100 o.h.e.jdbc.batch.internal.BatchingBatch : Executing batch size: 100 o.h.e.jdbc.batch.internal.BatchingBatch : Executing batch size: 100 o.h.e.jdbc.batch.internal.BatchingBatch : Executing batch size: 100
Не только это мы можем спасти 500
сущности во временных границах, установленных нашим SLA, но мы можем сэкономить в шесть раз больше сущностей без каких-либо проблем:
hypersistenceOptimizer.getEvents().clear(); storeService.insertAll(newStores(3000)); assertTrue(hypersistenceOptimizer.getEvents().isEmpty());
Теперь, если мы попытаемся спасти 3500
сущности:
hypersistenceOptimizer.getEvents().clear(); storeService.insertAll(newStores(3500)); assertTrue(hypersistenceOptimizer.getEvents().isEmpty());
Мы увидим, что тест завершается неудачно, так как оптимизатор Гиперсуществования обнаружил, что операция очистки контекста сохранения заняла более 500 миллисекунд:
Hypersistence Optimizer: CRITICAL - SessionFlushTimeoutEvent - Flushing the JPA EntityManager or Hibernate Session took [537] ms. The flush execution time impacts the overall transaction response time, so make sure that the current JPA EntityManager or Hibernate Session doesn't contain a very large number of entities. For more info about this event, check out this User Guide link -> https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#SessionFlushTimeoutEvent
Круто, правда?
Вывод
Оптимизатор сохраняемости Hy поддерживает гораздо больше проверок. Он может сканировать объекты и проверять, эффективно ли вы используете предложение JPA DISTINCT, а также предложение ORDER BY, и проверять количество результатов, возвращаемых любым заданным JPQL, API критериев или SQL-запросом.
Благодаря этим проверкам мониторинга производительности ваше приложение Spring Boot будет работать намного быстрее, и ваши клиенты будут иметь гораздо лучший опыт его использования.
Все оптимизации, представленные в этой статье, можно найти в этом репозитории GitHub .