digital-garden/_inbox/Потерянное обновление.md

107 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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)