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

Как зарегистрировать идентификатор транзакции базы данных с помощью MDC

Узнайте, как регистрировать идентификатор транзакции базы данных при печати выполненных инструкций SQL SLF4J MDC (Сопоставленный диагностический контекст).

Автор оригинала: 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, ознакомьтесь с этой статьей .