← Назад к вопросам

Что такое Dirty Write?

1.6 Junior🔥 201 комментариев
#Soft Skills и карьера

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Dirty Write: Проблема одновременного доступа к данным

Dirty Write (грязная запись) — это проблема конкурентности, которая возникает когда одна транзакция читает данные, которые были изменены, но ещё не зафиксированы (не committed) другой транзакцией. Это одна из самых критических проблем в многопоточных и многопроцессных системах работы с данными.

Определение Dirty Write

Dirty Write происходит когда:

  1. Транзакция A изменяет данные в базе
  2. Транзакция B читает эти измененные, но не зафиксированные данные
  3. Транзакция A откатывается (ROLLBACK)
  4. Транзакция 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 WriteDirty ReadNon-Repeatable ReadPhantom 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);
    // Блокировка будет отпущена при коммите транзакции
}

Лучшие практики

  1. Всегда используйте транзакции при работе с данными
  2. Выбирайте правильный уровень изоляции для вашего сценария
  3. Используйте FOR UPDATE для критичных операций
  4. Минимизируйте время блокировки — не держите транзакцию долго открытой
  5. Избегайте nested transactions и deadlocks через правильный порядок блокировок
  6. В Spring используйте @Transactional с правильным isolation level

Dirty Write — это серьезная проблема, которая автоматически предотвращается на уровне БД, но понимание её критично для правильной разработки многопоточных приложений.