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

Что такое проблема грязного чтения?

2.3 Middle🔥 121 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью

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

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

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

Что такое проблема грязного чтения?

Грязное чтение (dirty read) — это серьёзная проблема конкурентного доступа в базах данных, при которой одна транзакция читает незафиксированные (не закоммиченные) изменения, сделанные другой транзакцией. Если вторая транзакция позже откатится (rollback), прочитанные данные становятся некорректными.

Визуальный пример проблемы

Время  | Транзакция 1 (T1)          | Транзакция 2 (T2)
-------|---------------------------|---------------------------
T0     | BEGIN TRANSACTION          |
       |                            | BEGIN TRANSACTION
T1     | SELECT balance = 1000      |
       | (из базы)                  |
T2     |                            | UPDATE balance = 500
       |                            | (изменение, но не коммит)
T3     |                            | SELECT balance = 500
       |                            | ГРЯЗНОЕ ЧТЕНИЕ!
T4     |                            | ROLLBACK (отката
T5     | SELECT balance = ? (какое значение?)
       | (ожидает T2)               | Значение откачено!

Пример кода

// Транзакция 1: получение баланса счёта
public void transfer1() {
    Transaction tx1 = session.beginTransaction();
    try {
        Account account = session.get(Account.class, 1);
        System.out.println("T1: balance = " + account.getBalance());
        
        Thread.sleep(2000); // Ждём T2
        
        System.out.println("T1: balance = " + account.getBalance());
        // Может быть другое значение, если T2 изменил и откатил!
        
        tx1.commit();
    }
}

// Транзакция 2: изменение баланса и откат
public void transfer2() {
    Transaction tx2 = session.beginTransaction();
    try {
        Account account = session.get(Account.class, 1);
        account.setBalance(500); // Грязное изменение
        session.save(account);
        // Без коммита!
        
        Thread.sleep(500);
        
        tx2.rollback(); // Отката всех изменений
    }
}

Почему это проблема?

  1. Несогласованность данных — транзакция работает с данными, которые не будут существовать
  2. Потеря целостности — бизнес-логика может принять неправильные решения
  3. Каскадные ошибки — неправильные данные передаются дальше в систему

Пример из реальной жизни

// Система бронирования билетов

// Транзакция 1: проверяем количество билетов
int availableTickets = ticketRepository.countAvailable(eventId);
if (availableTickets > 0) {
    // Грязное чтение! T2 может откатить своё удаление
}

// Транзакция 2 (параллельно):
ticketRepository.deleteTicket(ticketId);
// ... какая-то ошибка ...
rollback(); // Билет был откачен

Уровни изоляции транзакций

Проблема грязного чтения решается установкой нужного уровня изоляции:

// Java (Hibernate)
session.setDefaultTransactionIsolationLevel(
    Connection.TRANSACTION_READ_COMMITTED
);
УровеньГрязное чтениеНеповторяемое чтениеФантомное чтение
READ_UNCOMMITTEDДАДАДА
READ_COMMITTEDНЕТДАДА
REPEATABLE_READНЕТНЕТДА
SERIALIZABLEНЕТНЕТНЕТ

READ_UNCOMMITTED (Грязное чтение возможно)

SET TRANSACTION ISOLATION LEVEL READ_UNCOMMITTED;
BEGIN TRANSACTION;
  SELECT balance FROM accounts WHERE id = 1; -- Может прочитать незакоммиченное
COMMIT;

READ_COMMITTED (Грязное чтение исключено)

SET TRANSACTION ISOLATION LEVEL READ_COMMITTED;
BEGIN TRANSACTION;
  SELECT balance FROM accounts WHERE id = 1; -- Читает только закоммиченные
COMMIT;

Решение в Java и Hibernate

// Установка уровня изоляции для всей сессии
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
    Account from = accountRepository.findById(fromId).orElseThrow();
    Account to = accountRepository.findById(toId).orElseThrow();
    
    // Теперь грязное чтение исключено
    from.setBalance(from.getBalance().subtract(amount));
    to.setBalance(to.getBalance().add(amount));
}

// На уровне JDBC
Connection conn = dataSource.getConnection();
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
Statement stmt = conn.createStatement();

Пример с Hibernate

@Entity
@Table(name = "accounts")
public class Account {
    @Id
    private Long id;
    private BigDecimal balance;
}

@Service
public class AccountService {
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = hibernateTemplate.find(Account.class, fromId);
        Account to = hibernateTemplate.find(Account.class, toId);
        
        // Оба счёта получены в READ_COMMITTED
        // Другие транзакции не могут влиять на наши данные
        
        from.setBalance(from.getBalance().subtract(amount));
        to.setBalance(to.getBalance().add(amount));
    }
}

Издержки повышения уровня изоляции

Уровень изоляции    | Защита        | Производительность | Блокировки
--------------------|---------------|--------------------|----------
READ_UNCOMMITTED     | Минимальная   | Максимальная       | Нет
READ_COMMITTED       | Средняя       | Высокая            | Да
REPEATABLE_READ      | Высокая       | Средняя            | Много
SERIALIZABLE         | Полная        | Низкая             | Максимум

Когда выбрать уровень изоляции

  • READ_UNCOMMITTED — только для отчётов с приблизительными данными
  • READ_COMMITTED — стандарт для большинства приложений (финансы, платежи)
  • REPEATABLE_READ — когда нужна согласованность одного ресурса
  • SERIALIZABLE — редко, только для критичных операций

Проблема грязного чтения — одна из самых серьёзных в многопользовательских системах, и её решение через правильный уровень изоляции транзакций — это критический аспект разработки надёжных приложений.