В этом посте я подробно расскажу, как Spring Framework обрабатывает транзакции за кулисами.

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

Первая концепция: изоляция КИСЛОТЫ.

Если вы не знаете, что означает« КИСЛОТА (атомарность, последовательность, изоляция, долговечность), я рекомендую поискать соответствующие материалы, потому что в этом посте я не буду углубляться в это содержание».

На этом этапе мы кратко поговорим об изоляции ACID, более конкретно о явлениях чтения и уровнях изоляции.

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

Среди этих явлений:

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

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

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

Среди уровней изоляции у нас есть:

Чтение незафиксированных: на этом уровне одновременные транзакции могут видеть незафиксированные данные между ними, что позволяет возникать феномену «грязного чтения».

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

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

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

Важно прояснить, что все, что было сказано выше, касающееся уровней изоляции, основано на том, что описано в стандарте ANSI, но каждая СУБД может реализовать уровни изоляции по-разному. Следовательно, мы можем вести себя по-разному в зависимости от используемой СУБД.

Еще один важный момент - каждая СУБД имеет уровень изоляции по умолчанию. Например, в Oracle, Postgres и SQL Server значение по умолчанию - Read Committed. В Mysql значение по умолчанию - Repeatable Read.

Зная, что всегда важно читать документацию для каждой СУБД, чтобы понимать, какие уровни изоляции она реализует и как обрабатывает каждое явление.

Вторая концепция: прокси фреймворка Spring

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

Говоря конкретно об аннотации @Transactional, когда вы аннотируете метод своего класса под капотом, Spring создает прокси на основе вашего класса и добавляет блоки кода для открытия и закрытия транзакции, выполняя вызов метод исходного класса в середине этого созданного блока кода.

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

Но когда создается этот прокси-сервер и как его выполняет?

Когда вы вводите класс, который вы создали, например, с помощью @Autowired, Spring анализирует, имеет ли внедряемый класс какой-либо тип аннотации, которую необходимо интерпретировать, и если да, вместо того, чтобы внедрять ваш класс, он внедряет прокси, который инкапсулирует всю логику оригинального класса.

Например, вы создали класс MyService типа @Service, в котором есть метод, аннотированный с помощью @Transactional, который записывает данные в базу данных, который называется saveData.

Теперь предположим, что у вас есть класс типа @Controller, который выполняет инъекцию класса MyService через аннотацию @Autowired.

Когда Spring собирается внедрить класс MyService, вместо того, чтобы внедрять исходный класс, он будет внедрять, например, класс MyService $$ EnhancerBySpringCGLIB, который также будет иметь метод saveData. Однако этот метод, в отличие от исходного метода класса, будет состоять из:

public void saveData (Object data) {
    // begin of transaction here
    originalClass.saveData(data);
    // commit or rollback of transaction here
}

ПРИМЕЧАНИЕ. Это просто дидактический пример, это не совсем оригинальный код, сгенерированный фреймворком.

Но как Spring обрабатывает транзакции?

Теперь, когда мы поверхностно рассмотрели эти две концепции, давайте поговорим о том, как Spring работает с транзакциями.

Вся магия происходит практически через аннотацию @Transactional.

Аннотацию @Transactional можно использовать как на уровне класса, так и на уровне метода, это будет зависеть от желаемой области транзакции.

Важный момент, который нужно знать, когда мы используем транзакции в Spring, связан с откатом транзакции. Откаты будут происходить только тогда, когда блок кода, помеченный @Transactional, выдает непроверенное исключение. Если вы хотите, чтобы откат происходил также в случае отмеченных исключений, вам необходимо указать исключение с помощью атрибута 'rollbackOn ' аннотации @Transactional, например:

@Transactional(rollbackOn = YourCheckedException.class)

У аннотации @Transactional есть два очень важных атрибута, которые мы подробно рассмотрим ниже, это изоляция и распространение.

Изоляция

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

Вы можете установить уровень изоляции в своих транзакциях Spring, заполнив значение атрибута изоляции возможными значениями ниже:

  • @Transactional (изоляция = Изоляция.READ_UNCOMMITTED)
  • @Transactional (изоляция = Изоляция.READ_COMMITTED)
  • @Transactional (изоляция = Изоляция.REPEATABLE_READ)
  • @Transactional (изоляция = Isolation.SERIALIZABLE)
  • @Transactional (изоляция = Isolation.DEFAULT)

Каждый из этих уровней, упомянутых выше, представляет поведение связанных уровней, реализованных в СУБД.

Если значение этого атрибута не заполнено, его значением по умолчанию будет Isolation.DEFAULT, что, следовательно, соответствует уровню изоляции СУБД по умолчанию.

Распространение

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

Возможные значения этого атрибута:

@Transactional (распространение = Propagation.REQUIRED)

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

@Transactional (распространение = Propagation.SUPPORTS)

В случае значения SUPPORTS он использует текущую транзакцию, если она есть, если транзакция не выполняется, она не создает новую.

@Transactional (распространение = Propagation.MANDATORY)

В ОБЯЗАТЕЛЬНОМ случае, если транзакция выполняется, она будет использоваться, в противном случае Spring выдаст исключение типа IllegalTransactionStateException.

@Transactional (распространение = Распространение. НИКОГДА)

В случае значения NEVER использование транзакции не будет разрешено, если транзакция выполняется, будет выброшено исключение типа IllegalTransactionStateException.

@Transactional (распространение = Propagation.NOT_SUPPORTED)

В NOT_SUPPORTED у нас есть сценарий, очень похожий на NEVER, но вместо того, чтобы генерировать исключение, Spring приостанавливает текущую транзакцию, если она существует.

@Transactional (распространение = Propagation.REQUIRES_NEW)

В случае REQUIRES_NEW, если транзакция выполняется, Spring приостановит ее и создаст новую. Как только эта новая транзакция завершится, Spring возобновит приостановленную транзакцию.

@Transactional (распространение = Propagation.NESTED)

Значение NESTED используется, когда вы хотите работать с точками сохранения, что дает возможность частичного отката. Когда транзакция не выполняется, она ведет себя так же, как REQUIRED, и создает новую транзакцию.

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

Возможно, вы заметили, что, когда мы говорим о значениях атрибута распространения, мы много говорим о текущих транзакциях, но какой будет текущая транзакция?

Текущие транзакции - это не что иное, как транзакции, поступающие извне, с более обширными областями, например, допустим, у вас есть класс типа @Controller, который использует аннотацию @Transactional, и этот контроллер выполняет вызов другого класса типа @Service который также использует аннотацию @Transactional. Когда происходит вызов от контроллера к службе и вызывается метод службы, уже существует текущая транзакция, поступающая из класса контроллера.

Заключение

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