Что такое грязное чтение в транзакциях?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Грязное чтение в транзакциях (Dirty Read)
Грязное чтение (Dirty Read) — это проблема конкурентности в базах данных, когда одна транзакция читает данные, которые были изменены другой транзакцией, но эти изменения ещё не были зафиксированы (не произошёл commit). Если изменяющая транзакция откатится, то читающая транзакция получит некорректные данные.
Концепция транзакций
Транзакция — это последовательность операций с БД, которые либо все выполняются успешно (commit), либо все откатываются (rollback). Транзакции должны быть ACID:
- Atomicity — «всё или ничего»
- Consistency — переход из одного стабильного состояния в другое
- Isolation — транзакции не мешают друг другу
- Durability — данные сохраняются после commit
Грязное чтение — это нарушение Isolation.
Пример грязного чтения
Время | Транзакция 1 | Транзакция 2
------|----------------------|----------------------
T1 | | SELECT balance FROM account WHERE id=1;
| | Result: 1000 руб
T2 | UPDATE account |
| SET balance = 500 |
| WHERE id=1; |
| (ещё не commit!) |
T3 | | SELECT balance FROM account WHERE id=1;
| | Result: 500 руб ← ГРЯЗНОЕ ЧТЕНИЕ!
T4 | ROLLBACK; |
| (транзакция отменена)|
T5 | | COMMIT;
| | Но баланс на самом деле 1000, не 500!
Транзакция 2 прочитала данные, которые были откачены. Это — грязное чтение.
Уровни изоляции транзакций
Базы данных предоставляют разные уровни изоляции для контроля конкурентности:
1. Read Uncommitted (Чтение неподтверждённых данных)
Самый низкий уровень изоляции. Позволяет грязные чтения.
-- PostgreSQL
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT * FROM users WHERE id = 1;
COMMIT;
-- MySQL
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT * FROM users WHERE id = 1;
COMMIT;
Проблемы:
- Грязные чтения ✗
- Фантомные чтения ✗
- Non-repeatable reads ✗
Когда использовать: очень редко, только для статистики с допуском неточности.
2. Read Committed (Чтение подтверждённых данных)
По умолчанию в PostgreSQL. Предотвращает грязные чтения.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM users WHERE id = 1; -- Читает только committed данные
COMMIT;
Преимущества:
- Нет грязных чтений ✓
- Хороший баланс производительности
Проблемы:
- Non-repeatable reads возможны
- Фантомные чтения возможны
3. Repeatable Read (Повторяемое чтение)
Используется по умолчанию в MySQL. Предотвращает грязные чтения и non-repeatable reads.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM users WHERE id = 1; -- Снимок данных
-- Другая транзакция может изменить эту строку
-- Но ВНУТРИ этой транзакции значение останется тем же
SELECT * FROM users WHERE id = 1; -- Тот же результат
COMMIT;
4. Serializable (Сериализуемость)
Высший уровень изоляции. Предотвращает все проблемы конкурентности.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT * FROM users WHERE id = 1;
-- Полная изоляция, как если бы транзакции выполнялись последовательно
COMMIT;
Преимущества:
- Нет грязных чтений ✓
- Нет non-repeatable reads ✓
- Нет фантомных чтений ✓
Недостатки:
- Очень медленно (много блокировок)
- Высокий риск deadlock'ов
Как грязное чтение происходит в коде
// Spring Boot + JPA
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
// Уровень изоляции не указан, используется default (READ_COMMITTED)
@Transactional
public void transferMoney(Long fromId, Long toId, double amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
from.setBalance(from.getBalance() - amount);
// На этом моменте другая транзакция может прочитать from
// и получить неправильный баланс (если эта транзакция откатится)
Account to = accountRepository.findById(toId).orElseThrow();
to.setBalance(to.getBalance() + amount);
accountRepository.save(from);
accountRepository.save(to);
}
// Грязное чтение может произойти здесь
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public double getBalance(Long accountId) {
Account account = accountRepository.findById(accountId).orElseThrow();
return account.getBalance(); // Может быть грязное значение
}
}
Правильное использование изоляции
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
// Безопасная транзакция
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transferMoney(Long fromId, Long toId, double amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
from.setBalance(from.getBalance() - amount);
Account to = accountRepository.findById(toId).orElseThrow();
to.setBalance(to.getBalance() + amount);
accountRepository.save(from);
accountRepository.save(to);
}
// Если нужна повышенная изоляция
@Transactional(isolation = Isolation.REPEATABLE_READ)
public double getBalance(Long accountId) {
Account account = accountRepository.findById(accountId).orElseThrow();
return account.getBalance();
}
// Для критичных операций
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalOperation() {
// Полная изоляция
}
}
Таблица уровней изоляции
Уровень | Грязное | Non-repeatable | Фантомное
| чтение | читание | чтение
---------------------|---------|----------------|----------
READ UNCOMMITTED | ✗ | ✗ | ✗
READ COMMITTED | ✓ | ✗ | ✗
REPEATABLE READ | ✓ | ✓ | ✗
SERIALIZABLE | ✓ | ✓ | ✓
Лучшие практики
- По умолчанию используйте READ_COMMITTED — хороший баланс
- Для критичных операций (финансы, права доступа) используйте REPEATABLE_READ или SERIALIZABLE
- Избегайте READ_UNCOMMITTED — почти никогда не нужен
- Не полагайтесь на уровни изоляции — используйте оптимистичные блокировки (версионирование)
- Тестируйте конкурентность — проверяйте race conditions
Альтернатива: Оптимистичное блокирование
Вместо пессимистичного блокирования (транзакции), используйте версионирование:
@Entity
public class Account {
@Id
private Long id;
private double balance;
@Version // Автоматическое версионирование
private Long version;
}
@Service
public class AccountService {
@Transactional
public void transferMoney(Long fromId, Long toId, double amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.setBalance(from.getBalance() - amount);
to.setBalance(to.getBalance() + amount);
// Если version был изменён другой транзакцией
// выбросится OptimisticLockingFailureException
accountRepository.saveAll(Arrays.asList(from, to));
}
}
Грязное чтение — это типичная проблема в многопроцессных системах, и её решение зависит от выбора правильного уровня изоляции транзакций.