Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Dirty Write: Проблема одновременного доступа к данным
Dirty Write (грязная запись) — это проблема конкурентности, которая возникает когда одна транзакция читает данные, которые были изменены, но ещё не зафиксированы (не committed) другой транзакцией. Это одна из самых критических проблем в многопоточных и многопроцессных системах работы с данными.
Определение Dirty Write
Dirty Write происходит когда:
- Транзакция A изменяет данные в базе
- Транзакция B читает эти измененные, но не зафиксированные данные
- Транзакция A откатывается (ROLLBACK)
- Транзакция B имеет невалидные данные (данные, которых в итоге нет в БД)
Пример Dirty Write
Сценарий: Перевод денег между счетами
Счет A: 1000 руб
Счет B: 500 руб
Транзакция 1: Перевести 200 руб с A на B
Транзакция 2: Перевести 100 руб с B на A
Проблема Dirty Write
Время | Транзакция 1 | Транзакция 2 | Счет A | Счет B
------|---------------------|---------------------|--------|--------
1 | BEGIN | | 1000 | 500
2 | | BEGIN | 1000 | 500
3 | UPDATE A SET = 800 | | 800 | 500 (не committed!)
4 | | READ A (800) | 800 | 500 (Dirty Read!)
5 | | UPDATE B = 600 | 800 | 600
6 | | COMMIT | 800 | 600
7 | ROLLBACK | | 1000 | 500 (откат изменений A)
8 | | (ошибка!) | 1000 | 600 (несогласованность!)
Транзакция 2 прочитала грязные данные и использовала их для своего расчета!
Различие между похожими проблемами
Dirty Read
читание незафиксированных данных другой транзакции:
// Транзакция A
Connection connA = dataSource.getConnection();
connA.setAutoCommit(false);
Statement stmtA = connA.createStatement();
// Изменяет, но не фиксирует
stmtA.executeUpdate("UPDATE balance SET amount = 500 WHERE id = 1");
// Не committed!
// Транзакция B (в другом потоке)
Connection connB = dataSource.getConnection();
Statement stmtB = connB.createStatement();
// Читает незафиксированное значение (если READ_UNCOMMITTED)
ResultSet rs = stmtB.executeQuery("SELECT amount FROM balance WHERE id = 1");
// Получит 500, хотя может быть откачено
connA.rollback(); // Откат! Транзакция B получила грязные данные
Dirty Write
запись данных, которые после этого откатываются:
// Транзакция A
Connection connA = dataSource.getConnection();
connA.setAutoCommit(false);
Statement stmtA = connA.createStatement();
// Записывает новое значение
stmtA.executeUpdate("UPDATE balance SET amount = 500 WHERE id = 1");
// Транзакция B начинает модифицировать уже измененные данные
Connection connB = dataSource.getConnection();
connB.setAutoCommit(false);
Statement stmtB = connB.createStatement();
// Переписывает данные, которые A изменила
stmtB.executeUpdate("UPDATE balance SET amount = 400 WHERE id = 1");
connB.commit(); // B зафиксирует свои изменения
connA.rollback(); // Но потом A откатывает свои изменения
// Теперь значение 400 от B, но основано на промежуточном состоянии
Уровни изоляции транзакций и Dirty Write
Все уровни изоляции предотвращают Dirty Write:
| Уровень | Dirty Write | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|---|
| READ_UNCOMMITTED | НЕТ | ДА | ДА | ДА |
| READ_COMMITTED | НЕТ | НЕТ | ДА | ДА |
| REPEATABLE_READ | НЕТ | НЕТ | НЕТ | ДА |
| SERIALIZABLE | НЕТ | НЕТ | НЕТ | НЕТ |
Key insight: даже READ_UNCOMMITTED предотвращает Dirty Write! Это гарантировано на уровне БД.
Механизм предотвращения Dirty Write
Write Locks (блокировки записи)
Большинство БД используют эксклюзивные блокировки:
// Java + JDBC пример
Connection conn = dataSource.getConnection();
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
// Эта операция получает WRITE LOCK на строку
stmt.executeUpdate("UPDATE account SET balance = 800 WHERE id = 1");
// Другая транзакция НЕ может писать в эту строку до COMMIT или ROLLBACK
// (и не может читать, если используется SERIALIZABLE)
conn.commit(); // Блокировка отпускается
SELECT FOR UPDATE
Для явного получения блокировки при чтении:
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
// Получить блокировку записи ДО обновления
ResultSet rs = stmt.executeQuery(
"SELECT balance FROM account WHERE id = 1 FOR UPDATE"
);
rs.next();
int currentBalance = rs.getInt("balance");
// Теперь безопасно обновить
stmt.executeUpdate(
"UPDATE account SET balance = " + (currentBalance + 100) + " WHERE id = 1"
);
conn.commit();
Пример: Деньги на счетах (правильная реализация)
public void transferMoney(int fromAccountId, int toAccountId, int amount)
throws SQLException {
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
try {
Statement stmt = conn.createStatement();
// Блокируем оба счета
ResultSet rs1 = stmt.executeQuery(
"SELECT balance FROM account WHERE id = " + fromAccountId + " FOR UPDATE"
);
rs1.next();
int fromBalance = rs1.getInt("balance");
if (fromBalance < amount) {
throw new IllegalArgumentException("Insufficient funds");
}
ResultSet rs2 = stmt.executeQuery(
"SELECT balance FROM account WHERE id = " + toAccountId + " FOR UPDATE"
);
rs2.next();
int toBalance = rs2.getInt("balance");
// Обновляем оба счета
stmt.executeUpdate(
"UPDATE account SET balance = " + (fromBalance - amount) +
" WHERE id = " + fromAccountId
);
stmt.executeUpdate(
"UPDATE account SET balance = " + (toBalance + amount) +
" WHERE id = " + toAccountId
);
conn.commit();
} catch (Exception e) {
conn.rollback();
throw e;
} finally {
conn.close();
}
}
Dirty Write в ORM (Hibernate/JPA)
@Transactional
public void updateUser(Long userId, String newEmail) {
// Hibernate автоматически управляет блокировками
User user = userRepository.findById(userId).orElseThrow();
// Hibernate получит WRITE LOCK при загрузке (если нужно)
user.setEmail(newEmail);
// При commit() изменения будут записаны и committed
} // Блокировка автоматически отпустится
Если нужна явная блокировка в Spring Data:
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT u FROM User u WHERE u.id = :id")
User findUserWithLock(@Param("id") Long id);
@Transactional
public void updateWithLock(Long userId, String newEmail) {
User user = userRepository.findUserWithLock(userId);
user.setEmail(newEmail);
// Блокировка будет отпущена при коммите транзакции
}
Лучшие практики
- Всегда используйте транзакции при работе с данными
- Выбирайте правильный уровень изоляции для вашего сценария
- Используйте FOR UPDATE для критичных операций
- Минимизируйте время блокировки — не держите транзакцию долго открытой
- Избегайте nested transactions и deadlocks через правильный порядок блокировок
- В Spring используйте @Transactional с правильным isolation level
Dirty Write — это серьезная проблема, которая автоматически предотвращается на уровне БД, но понимание её критично для правильной разработки многопоточных приложений.