Автор оригинала: Vlad Mihalcea.
Вступление
В этой статье я покажу вам, как можно зарегистрировать идентификатор транзакции базы данных, связанный с данным оператором SQL, с помощью функции MDC (Сопоставленный диагностический контекст), предлагаемой многими системами ведения журнала.
Если вы пишете код доступа к данным, вы должны использовать ведение журнала. Как я объяснил в этой статье , использование прокси-сервера JDBC, такого как datasourceproxy
или p6spy
, является лучшим подходом к регистрации операторов SQL.
С помощью datasourceproxy
вы можете легко создать расширение JUnit для автоматического обнаружения проблем с запросами N+1 . Для получения более подробной информации ознакомьтесь с проектом db-util с открытым исходным кодом.
Поскольку JPA и Hibernate генерируют инструкции SQL от вашего имени на основе переходов состояния сущности, выполняемых уровнем доступа к данным, использование ведения журнала является обязательным, поскольку вам необходимо подтвердить, что автоматически сгенерированные инструкции SQL являются эффективными и действенными.
Руководство по регистрации идентификатора транзакции базы данных с помощью инструкции SQL с использованием функции MDC (Сопоставленный диагностический контекст). @vlad_mihalcea https://t.co/3sfwl6XRpz
Вариант использования конфликта блокировки базы данных
Теперь давайте предположим, что у нас есть следующая ситуация:
doInJPA(entityManager -> { Post post = entityManager .createQuery( "select p " + "from Post p " + "where p.id = :id", Post.class) .setParameter("id", 1L) .setLockMode(LockModeType.PESSIMISTIC_WRITE) .getSingleResult(); try { executeSync(() -> { doInJPA(_entityManager -> { Post _post = (Post) _entityManager .createQuery( "select p " + "from Post p " + "where p.id = :id", Post.class) .setParameter("id", 1L) .unwrap(org.hibernate.query.Query.class) .setLockOptions( new LockOptions() .setLockMode(LockMode.PESSIMISTIC_WRITE) .setTimeOut(LockOptions.NO_WAIT) ) .getSingleResult(); }); }); } catch (Exception expected) { assertTrue( ExceptionUtil .rootCause(expected) .getMessage() .contains( "could not obtain lock on row in relation" ) ); } });
Первый пользователь, Алиса, блокирует запись post
со значением идентификатора 1
. После этого второй пользователь, Боб, пытается заблокировать ту же запись post
, но, поскольку он использует директиву NO_WAIT
lock, он сразу же получит исключение LockAquisitionException
.
При запуске вышеупомянутого тестового набора Hibernate создает следующие записи в журнале:
DEBUG [Alice]: n.t.d.l.SLF4JQueryLoggingListener - Time:3, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:[" SELECT p.id AS id1_0_, p.title AS title2_0_, p.version AS version3_0_ FROM post p WHERE p.id = ? FOR UPDATE OF p "], Params:[( 1 )] DEBUG [Bob]: n.t.d.l.SLF4JQueryLoggingListener - Time:0, Success:False, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:[" SELECT p.id AS id1_0_, p.title AS title2_0_, p.version AS version3_0_ FROM post p WHERE p.id = ? FOR UPDATE OF p NOWAIT "], Params:[( 1 )] WARN [Bob]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 0, SQLState: 55P03 ERROR [Bob]: o.h.e.j.s.SqlExceptionHelper - ERROR: could not obtain lock on row in relation "post"
Обратите внимание, что второй запрос имеет Успех
статус Ложь
, так как Исключение SQLException
возникает с не удалось получить блокировку строки в отношении "сообщение"
сообщение.
Теперь мы хотим добавить идентификатор транзакции базы данных в сообщения журнала инструкций SQL, чтобы мы могли сопоставить выполненные SQL-запросы с соответствующими транзакциями.
Для этой цели мы можем использовать MDC.
Что такое MDC
MDC (Сопоставленный диагностический контекст) предназначен для регистрации того, что ThreadLocal
относится к потокам Java. В принципе, MDC позволяет вам регистрировать пары ключ/значение, которые ограничены текущим потоком и на которые вы можете ссылаться, когда платформа ведения журнала создает сообщения журнала.
Поскольку SLF4J (Простой фасад ведения журнала) является очень удобным адаптером ведения журнала Java, он стал очень популярным выбором для многих приложений Java, а также обеспечивает поддержку MDC через класс org.slf4j.MDC
.
Итак, чтобы задать заданную переменную журнала с помощью MDC, вы можете использовать метод put
, как показано в следующем примере:
MDC.put("txId", String.format(" TxId: [%s]", transactionId(entityManager)));
Чтобы получить идентификатор базовой транзакции базы данных, вам необходимо использовать запрос, относящийся к конкретной базе данных. Для получения более подробной информации ознакомьтесь с этой статьей .
Чтобы напечатать переменную журнала “txId” в журнале, нам нужно включить эту переменную в шаблон добавления журнала:
TRACE %-5p [%t]:%X{txId} %c{1} - %m%n UTF-8
Шаблон %X{txId}
используется для ссылки на переменную txId
журнала.
Регистрация идентификатора текущей транзакции базы данных
Теперь мы можем изменить предыдущий тестовый случай, чтобы включить переменную MDC txId
log:
doInJPA(entityManager -> { try(MDC.MDCCloseable closable = MDC .putCloseable( "txId", String.format( " TxId: [%s]", transactionId(entityManager) ) ) ){ Post post = entityManager .createQuery( "select p " + "from Post p " + "where p.id = :id", Post.class) .setParameter("id", 1L) .setLockMode(LockModeType.PESSIMISTIC_WRITE) .getSingleResult(); try { executeSync(() -> { doInJPA(_entityManager -> { try(MDC.MDCCloseable _closable = MDC .putCloseable( "txId", String.format( " TxId: [%s]", transactionId(_entityManager) ) ) ) { Post _post = (Post) _entityManager .createQuery( "select p " + "from Post p " + "where p.id = :id", Post.class) .setParameter("id", 1L) .unwrap(org.hibernate.query.Query.class) .setLockOptions( new LockOptions() .setLockMode(LockMode.PESSIMISTIC_WRITE) .setTimeOut(LockOptions.NO_WAIT) ) .getSingleResult(); } }); }); } catch (Exception expected) { assertTrue( ExceptionUtil .rootCause(expected) .getMessage() .contains("could not obtain lock on row in relation") ); } } });
Обратите внимание, что мы используем метод MDC.put Closeable
в блоке “попытка с ресурсами”. Таким образом, мы удостоверяемся, что переменная this
удалена из хранилища MDC
, чтобы она не была добавлена в журнал после выхода из блока “попытка с ресурсами”.
При запуске приведенного выше тестового случая Hibernate создаст следующие записи в журнале:
DEBUG [Alice]: n.t.d.l.SLF4JQueryLoggingListener - Time:1, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:[" SELECT CAST(txid_current() AS text) "], Params:[()] DEBUG [Alice]: TxId: [796989] n.t.d.l.SLF4JQueryLoggingListener - Name:DATA_SOURCE_PROXY, Time:3, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:[" SELECT p.id AS id1_0_, p.title AS title2_0_, p.version AS version3_0_ FROM post p WHERE p.id = ? FOR UPDATE OF p "], Params:[( 1 )] DEBUG [Bob]: n.t.d.l.SLF4JQueryLoggingListener - Time:1, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:[" SELECT CAST(txid_current() AS text) "], Params:[()] DEBUG [Bob]: TxId: [796990] n.t.d.l.SLF4JQueryLoggingListener - Time:0, Success:False, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:[" SELECT p.id AS id1_0_, p.title AS title2_0_, p.version AS version3_0_ FROM post p WHERE p.id = ? FOR UPDATE OF p NOWAIT "], Params:[( 1 )] WARN [Bob]: TxId: [796990] o.h.e.j.s.SqlExceptionHelper - SQL Error: 0, SQLState: 55P03 ERROR [Bob]: TxId: [796990] o.h.e.j.s.SqlExceptionHelper - ERROR: could not obtain lock on row in relation "post"
Как я объяснил в этой статье , SELECT CAST(txid_current() В ВИДЕ текста)
используется в PostgreSQL для получения идентификатора транзакции базовой базы данных.
Обратите внимание, что на этот раз Третья
запись добавляется для каждой инструкции SQL, выполняемой после установки переменной журнала txId
MDC.
Потрясающе, правда?
Вывод
Механизм переменной журнала MDC очень полезен для передачи контекстно-зависимой информации в записи журнала. Используя MDC, мы можем легко добавить идентификатор транзакции базы данных для каждой выполняемой инструкции SQL.
Поскольку это сквозная проблема, мы могли бы инкапсулировать переменную журнала MDC put
и удалить
вызовы методов в AOP Аспект, который перехватывает все вызовы методов доступа к данным. Для получения более подробной информации о том, как вы могли бы написать такой аспект, используя поддержку Spring AOP, ознакомьтесь с этой статьей .