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

Блокировка метаданных MySQL и завершение транзакции базы данных

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

Как ранее объяснялось , каждая инструкция SQL должна выполняться в контексте транзакции базы данных. Для изменения инструкций (например, ВСТАВКА , ОБНОВЛЕНИЕ , УДАЛЕНИЕ ) необходимо использовать блокировки на уровне строк, чтобы обеспечить возможность восстановления и избежать аномалий данных.

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

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

protected void doInJPA(JPATransactionVoidFunction function) {
    EntityManager entityManager = null;
    EntityTransaction txn = null;
    try {
        entityManager = emf.createEntityManager();
        function.beforeTransactionCompletion();
        txn = entityManager.getTransaction();
        txn.begin();
        function.accept(entityManager);
        txn.commit();
    } catch (RuntimeException e) {
        if ( txn != null && txn.isActive()) txn.rollback();
        throw e;
    } finally {
        function.afterTransactionCompletion();
        if (entityManager != null) {
            entityManager.close();
        }
    }
}

Учитывая, что у нас есть следующая организация JPA:

@Entity(name = "DateEvent")
public class DateEvent {

    @Id
    @GeneratedValue
    private Long id;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "`timestamp`")
    private Calendar timestamp;

    public DateEvent() {
    }

    public DateEvent(Calendar timestamp) {
        this.timestamp = timestamp;
    }

    public Long getId() {
        return id;
    }

    public Calendar getTimestamp() {
        return timestamp;
    }
}

Модульный тест выглядит следующим образом:

@Test
public void testEqualityAfterPersisting() {
    final Calendar calendar = new GregorianCalendar();
    doInJPA(entityManager -> {
        entityManager.persist( new DateEvent( calendar ) );
    } );
    doInJPA(entityManager -> {
        DateEvent dateEvent = entityManager.createQuery(
            "select e from DateEvent e", DateEvent.class )
        .getSingleResult();
        assertEquals( calendar, dateEvent.getTimestamp() );
    } );
}

Этот тест успешно выполняется на HSQLDB, H2, Oracle и PostgreSQL. В MySQL тест застрял, и журнал выглядит так:

DEBUG [main]: o.h.SQL - select next_val as id_val from hibernate_sequence for update
DEBUG [main]: o.h.SQL - update hibernate_sequence set next_val= ? where next_val=?
DEBUG [main]: o.h.SQL - insert into DateEvent (`timestamp`, id) values (?, ?)
INFO  [main]: o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397: Using ASTQueryTranslatorFactory
DEBUG [main]: o.h.SQL - select calendarwi0_.id as id1_0_, calendarwi0_.`timestamp` as timestam2_0_ from DateEvent calendarwi0_
INFO  [main]: o.h.t.s.i.SchemaDropperImpl$DelayedDropActionImpl - HHH000477: Starting delayed drop of schema as part of SessionFactory shut-down'
DEBUG [main]: o.h.SQL - drop table if exists DateEvent

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

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

В этом случае следующее утверждение не выполняется:

assertEquals( calendar, dateEvent.getTimestamp() );

Но ComparisonFailure расширяет Ошибку утверждения , которая, в свою очередь, расширяет Ошибку . Таким образом, ошибка утверждения не будет поймана блоком catch , и транзакция не получит возможности отката.

наконец блок всегда выполняется, даже если возникает Ошибка . В наконец блоке EntityManager закрыт, и, поскольку я использую hibernate.hbm2ddl.auto установлен в create-drop , Hibernate пытается удалить текущую схему.

Но почему он блокируется? Это можно было бы ожидать в реализации двухфазной блокировки с возможностью повторного чтения, когда операция чтения выполняет общую блокировку результирующего набора. Но механизм хранения InnoDB использует MVCC , поэтому читатели не должны блокировать записи, и здесь нет параллельных транзакций.

В этом примере блокировка происходит при использовании одного сеанса базы данных. Даже если оператор select не получает блокировку на уровне строк, MySQL все равно использует блокировку метаданных для каждой транзакции базы данных, и в соответствии с документацией MySQL:

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

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

catch (Throwable e) {
    if ( txn != null && txn.isActive()) txn.rollback();
    throw e;
}

Теперь при выполнении теста мы получаем следующий журнал:

DEBUG [main]: o.h.SQL - update hibernate_sequence set next_val= ? where next_val=?
DEBUG [main]: o.h.SQL - insert into DateEvent (`timestamp`, id) values (?, ?)
INFO  [main]: o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397: Using ASTQueryTranslatorFactory
DEBUG [main]: o.h.SQL - select calendarwi0_.id as id1_0_, calendarwi0_.`timestamp` as timestam2_0_ from DateEvent calendarwi0_
INFO  [main]: o.h.t.s.i.SchemaDropperImpl$DelayedDropActionImpl - HHH000477: Starting delayed drop of schema as part of SessionFactory shut-down'
DEBUG [main]: o.h.SQL - drop table if exists DateEvent
DEBUG [main]: o.h.SQL - drop table if exists hibernate_sequence

java.lang.AssertionError: 
Expected :java.util.GregorianCalendar[time=1455774800060,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Europe/Athens",offset=7200000,dstSavings=3600000,useDaylight=true,transitions=138,lastRule=java.util.SimpleTimeZone[id=Europe/Athens,offset=7200000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2016,MONTH=1,WEEK_OF_YEAR=8,WEEK_OF_MONTH=3,DAY_OF_MONTH=18,DAY_OF_YEAR=49,DAY_OF_WEEK=5,DAY_OF_WEEK_IN_MONTH=3,AM_PM=0,HOUR=7,HOUR_OF_DAY=7,MINUTE=53,SECOND=20,MILLISECOND=60,ZONE_OFFSET=7200000,DST_OFFSET=0]
Actual   :java.util.GregorianCalendar[time=1455774800000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Europe/Athens",offset=7200000,dstSavings=3600000,useDaylight=true,transitions=138,lastRule=java.util.SimpleTimeZone[id=Europe/Athens,offset=7200000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2016,MONTH=1,WEEK_OF_YEAR=8,WEEK_OF_MONTH=3,DAY_OF_MONTH=18,DAY_OF_YEAR=49,DAY_OF_WEEK=5,DAY_OF_WEEK_IN_MONTH=3,AM_PM=0,HOUR=7,HOUR_OF_DAY=7,MINUTE=53,SECOND=20,MILLISECOND=0,ZONE_OFFSET=7200000,DST_OFFSET=0]

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

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