--- aliases: - lost update tags: - зрелость/🌱 date: - - 2024-06-19 zero-link: - "[[00 Базы Данных]]" parents: - "[[Проблемы при параллельном выполнении нескольких транзакций]]" linked: link: https://struchkov.dev/blog/ru/transactional-isolation-levels/#%D0%BF%D0%BE%D1%82%D0%B5%D1%80%D1%8F%D0%BD%D0%BD%D0%BE%D0%B5-%D0%BE%D0%B1%D0%BD%D0%BE%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5 --- **Потерянное обновление (lost update).** Две параллельные транзакции меняют одни и те же данные, при этом итоговый результат обновления предсказать невозможно. ^23d01d Рассмотрим эту проблему на примере: ```java public class LostUpdateExample { public static final String READ = "SELECT person.balance FROM person WHERE id = ?"; public static final String UPDATE = "UPDATE person SET balance = ? WHERE id = ?"; @SneakyThrows public static void main(String[] args) { // Начинаем две транзакции. final Connection connectionOne = getNewConnection(); final Connection connectionTwo = getNewConnection(); // Первая и вторая транзакция запрашивают баланс пользователя. // balance = 1000 final long balanceOne = getBalance(connectionOne); final long balanceTwo = getBalance(connectionTwo); // Первая транзакция готовится обновить баланс пользователю. final PreparedStatement updateOne = connectionOne.prepareStatement(UPDATE); updateOne.setLong(1, balanceOne + 10); updateOne.setLong(2, 1); updateOne.execute(); // Первая транзакция фиксирует изменения и завершается. // Значение balance в базе в этот момент = 1010. connectionOne.commit(); connectionOne.close(); // Но вторая транзакция ничего не знает про изменения в БД. // Значение balanceTwo все еще равно 1000, к этому значению мы добавляем 5. final PreparedStatement updateTwo = connectionTwo.prepareStatement(UPDATE); updateTwo.setLong(1, balanceTwo + 5); updateTwo.setLong(2, 1); updateTwo.execute(); // Вторая транзакция фиксирует свои изменения и завершается. // В итоге в БД остается значение 1005, а не 1015, как хотелось бы нам. connectionTwo.commit(); connectionTwo.close(); } private static Connection getNewConnection() throws SQLException { final Connection connection = Repository.getConnection(); connection.setAutoCommit(false); connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); return connection; } private static long getBalance(Connection connectionOne) throws SQLException { final PreparedStatement preparedStatement = connectionOne.prepareStatement(READ); preparedStatement.setLong(1, 1); final ResultSet resultSet = preparedStatement.executeQuery(); resultSet.next(); final long balanceOne = resultSet.getLong(1); return balanceOne; } } ``` На строках 10, 11 мы получаем 2 независимых соединения с БД, в которых отключён auto-commit и установлен уровень изоляции [Read committed](Read%20committed.md). На строках 15 и 16 обе транзакции получают баланс первого пользователя. Напомню, что баланс равен 1000. На строках 19-22 выполняется обновление баланса пользователя первой транзакцией. Для этого к полученному ранее балансу прибавляется 10. Но сейчас эти изменения не были отправлены в БД. Это произойдёт только при закрытии транзакции. Закрываем первую транзакцию (26, 27). Баланс пользователя в БД равен 1010. Но вторая транзакция идёт параллельно, и ничего не знает об изменении баланса пользователя. Ведь баланс мы уже считали из БД, и там было 1000. Вторая транзакция прибавляет к балансу пользователя 5 (31-34). После чего вторая транзакция также закрывается (38, 39). Баланс пользователя в БД равен 1005. Мы потеряли обновления, которые выполнила первая транзакция. Такое поведение называют [Race condition](Race%20condition.md). Изменим уровень транзакций на более изолированный. В примере у нас используется `READ_COMMITTED`, установим [Repeatable read](Repeatable%20read.md) в строке 45. В таком случае при выполнении нашего кода получаем исключение `PSQLException`. ![](Pasted%20image%2020240619201135.png) Защита от потерянных обновлений: - Атомарные операции - Явные блокировки ``` SELECT * FROM tb1 WHERE id = 10 FOR UPDATE ``` - Автоматическое обнаружение - CAS операции ## Дополнительные материалы - [Пример на Java](https://github.com/Example-uPagge/transactional/blob/master/jdbc-transaction/src/main/java/dev/struchkov/example/transaction/problems/LostUpdateExample.java) - ![](Pasted%20image%2020240620094127.png)