digital-garden/_inbox/Неповторяющееся чтение.md

4.9 KiB
Raw Blame History

aliases tags date zero-link parents linked link
non-repeatable reads
зрелость/🌱
2024-06-19
00 Базы Данных
Проблемы при параллельном выполнении нескольких транзакций
https://struchkov.dev/blog/ru/transactional-isolation-levels/#%D0%BD%D0%B5%D0%BF%D0%BE%D0%B2%D1%82%D0%BE%D1%80%D1%8F%D1%8E%D1%89%D0%B5%D0%B5%D1%81%D1%8F-%D1%87%D1%82%D0%B5%D0%BD%D0%B8%D0%B5

Неповторяющееся чтение (non-repeatable reads). Запрос с одними и теми же условиями даёт неодинаковые результаты в рамках транзакции. ^3e8781

Эта проблема присутствует на уровне изоляции Read committed и Read uncommitted.

Рассмотрим пример. Начинаем первую транзакцию. Считываем баланс пользователя, получаем значение 1000. Далее стартует вторая транзакция в отдельном потоке, а текущий поток засыпает.

Во второй транзакции устанавливаем пользователю баланс равный 100_000. После чего закрываем вторую транзакцию. Баланс успешно обновился в БД.

Первая транзакция продолжает выполнение. Снова запрашивает баланс пользователя из БД, на этот раз получает значение 100_000. Таким образом, вторая транзакция повлияла на выполнение первой.

public class NonRepeatableRead {

    private static final int ISOLATION_LEVEL = Connection.TRANSACTION_READ_COMMITTED;

    public static void main(String[] args) {
        try (
                final Connection connection = Repository.getConnection();
                final Statement statement = connection.createStatement()
        ) {
            connection.setAutoCommit(false);
            connection.setTransactionIsolation(ISOLATION_LEVEL);

            final ResultSet resultSetOne = statement.executeQuery("SELECT * FROM person WHERE id = 1");
            while (resultSetOne.next()) {
                final String balance = resultSetOne.getString("balance");
                System.out.println("[one] Balance: " + balance);
            }

            new OtherTransaction().start();
            Thread.sleep(2000);

            final ResultSet resultSetTwo = statement.executeQuery("SELECT * FROM person WHERE id = 1");
            while (resultSetTwo.next()) {
                final String balance = resultSetTwo.getString("balance");
                System.out.println("[one] Balance: " + balance);
            }

        } catch (SQLException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    static class OtherTransaction extends Thread {
        @Override
        public void run() {
            try (
                    final Connection connection = Repository.getConnection();
                    final Statement statement = connection.createStatement()
            ) {
                connection.setAutoCommit(false);
                connection.setTransactionIsolation(ISOLATION_LEVEL);

                statement.executeUpdate("UPDATE person SET balance = 100000 WHERE id = 1");
                connection.commit();

                final ResultSet resultSetTwo = statement.executeQuery("SELECT * FROM person WHERE id = 1");
                while (resultSetTwo.next()) {
                    final String balance = resultSetTwo.getString("balance");
                    System.out.println("[two] Balance: " + balance);
                }

                connection.commit();
            } catch (SQLException e) {
                System.out.println(e.getMessage());
            }
        }
    }

}

Результат работы примера:

[one] Balance: 1000
[two] Balance: 100000
[one] Balance: 100000

Для устранения этой проблемы воспользуемся известным нам уровнем изоляции Repeatable read.

Тогда при выполнении примера первая транзакция дважды получит значение баланса равным 1000, несмотря на то, что в БД будет уже 100_000.

Результат работы примера с REPEATABLE_READ:

[one] Balance: 1000
[two] Balance: 100000
[one] Balance: 1000

Дополнительные материалы