107 lines
6.1 KiB
Markdown
107 lines
6.1 KiB
Markdown
---
|
||
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) |