Изоляция транзакций в Java. Грязное чтение

Две или более выполняющиеся параллельно транзакции часто должны быть изолированы друг от друга.

Example

Есть три случая когда необходимо изолировать транзакции друг от друга:

  • грязное чтение,
  • неповторяющееся чтение
  • фантомное чтение.

Начнем с грязного чтения.

В прошлых уроках о транзакциях мы говорили, что выполнение группы запросов, и соответственно изменение БД происходит только после вызова commit.

Но транзакции могут вести себя и по другому.

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

По умолчанию, как уже можно было понять, стоит первый вариант из двух приведенных. Он неявно установлен с помощью поля TRANSACTION_READ_COMMITTED.

Первый вариант исключает возможность грязного чтения. При втором же варианте возможно грязное чтение. Его уже нужно устанавливать явно с помощью поля TRANSACTIONS_READ_UNCOMMITED.


Грязное чтение

Допустим есть транзакция А и транзакция В, которые выполняются в разных потоках параллельно при этом обе транзакции выполняются в режиме TRANSACTIONS_READ_UNCOMMITED, то есть грязное чтение возможно.

К транзакции А в потоке где она выполняется будет впоследствии применен rollback.

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

Но пока rollback не произошел к БД, которая была изменена транзакцией А могут совершать запросы другие потоки, например, поток в котором выполняется транзакция В.

Очевидно, что если транзакция В обратиться к данным в БД, которые изменила транзакция А и которые в результате окажутся недействительными из-за примененного к этим данным rollback, это может быть очень нежелательным. То есть транзакция В будет работать с данными, которых в БД в результате не будет, так как к ним будет применен rollback.

Давайте уберем грязное чтение

Как уже говорилось, по умолчанию стоит TRANSACTION_READ_COMMITTED, поэтому грязного чтения не будет даже если не устанавливать TRANSACTION_READ_COMMITTED явно.

Пример программы:

import java.sql.*; public class IsolationsDirty { public static void main(String[] args) throws ClassNotFoundException, SQLException, InterruptedException { //здесь идет транзакция А Class.forName(“com.mysql.cj.jdbc.Driver”); Connection connection = DriverManager.getConnection( “jdbc:mysql://localhost/storage”, “root”, “07998MSD”); Statement statement = connection.createStatement(); connection.setAutoCommit(false); //установим режим транзакции при котором //теперь невозможно грязное чтение. //На самом деле TRANSACTION_READ_COMMITTED //как уже говорилось установлен по умолчанию //поэтому эту строчку кода можно не писать. connection.setTransactionIsolation( Connection.TRANSACTION_READ_COMMITTED); //изменим имя второй книги в таблице //Важно что теперь грязное чтение невозможно //то есть запрос ниже выполнится только при следующем //коммите. То есть транзакция В не будет читать //данные, которые в итоге окажутся недействительными statement.executeUpdate(“update books set name = ” +”‘another name’ where id = 2″); //Запускаем поток транзакции В new TransactionB().start(); //при этом на две секунды останавливаем //поток транзакции А, то есть текущий. Thread.sleep(2000); //После того как данные книг считаны //транзакцией В, с помощью rollback делаем //так чтобы изменения БД, которые должна сделать //транзакция А при следующем коммите //(то есть изменение второй книги) не случились. connection.rollback(); } static class TransactionB extends Thread { @Override public void run() { //здесь идет транзакция В try { Connection connection = DriverManager.getConnection( “jdbc:mysql://localhost/storage”, “root”, “07998MSD”); Statement statement = connection.createStatement(); connection.setAutoCommit(false); connection.setTransactionIsolation( Connection.TRANSACTION_READ_COMMITTED); //Пока транзакция А остановлена на 2 сек //Извлекаем и выводим на консоль данные книг. //В этом случае данные второй книги //не изменены в БД транзакцией А. //Так как чтобы они вступили в силу нужно //чтобы случился коммит транзакции А. ResultSet resultSet = statement.executeQuery(“SELECT * FROM books”); while (resultSet.next()) { System.out.println(“name: ” + resultSet.getString(“name”)); } } catch (SQLException e){} } } }

Скомпилируем, запустим программу:

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

Изоляция транзакций. Неповторяющееся чтение

Объяснение неповторяющегося чтения в JDBC. Используйте TRANSACTION_REPEATABLE_READ для согласованности данных между SELECT-запросами.
Time to read: 15

Изоляция транзакций. Фантомное чтение

Разберитесь с фантомным чтением в транзакциях JDBC. Как TRANSACTION_SERIALIZABLE предотвращает вставку данных во время выполнения запросов.
Time to read: 14

Безопасные запросы с PreparedStatement

Узнайте, как PreparedStatement в JDBC защищает от SQL-инъекций. Примеры уязвимого кода и безопасной реализации для работы с базой данных в Java.
Time to read: 12