Какие проблемы решают каждый уровень изоляции
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Уровни изоляции транзакций в БД
Уровни изоляции (isolation levels) в базах данных решают разные проблемы конкурентного доступа к данным. Это баланс между производительностью и консистентностью. Разберём каждый.
Проблемы без изоляции
Dirty Read (грязное чтение)
Транзакция читает не закоммиченные изменения другой транзакции:
-- Транзакция 1
BEGIN TRANSACTION;
UPDATE accounts SET balance = 5000 WHERE id = 1;
-- Баланс изменён на 5000, но не закоммичен
-- Транзакция 2 (в это же время)
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- Видит 5000 (не закоммичено!)
-- Транзакция 1 откатывается
ROLLBACK;
-- Теперь баланс обратно к 4000
-- Транзакция 2 работает с неверным значением 5000 (ghost data)
COMMIT;
Non-Repeatable Read (нестабильное чтение)
Транзакция читает одни и те же данные дважды, получая разные результаты:
-- Транзакция 1
BEGIN TRANSACTION;
SELECT name FROM users WHERE id = 1; -- Результат: "Alice"
-- Транзакция 2 (в это же время)
BEGIN TRANSACTION;
UPDATE users SET name = "Alicia" WHERE id = 1;
COMMIT;
-- Транзакция 1 (продолжается)
SELECT name FROM users WHERE id = 1; -- Результат: "Alicia" (изменилось!)
COMMIT;
Phantom Read (фантомное чтение)
Транзакция читает набор строк дважды, а во второй раз появляются/исчезают новые строки:
-- Транзакция 1
BEGIN TRANSACTION;
SELECT COUNT(*) FROM users WHERE age > 18; -- Результат: 100
-- Транзакция 2 (в это же время)
BEGIN TRANSACTION;
INSERT INTO users (age) VALUES (25);
COMMIT; -- Добавили пользователя
-- Транзакция 1 (продолжается)
SELECT COUNT(*) FROM users WHERE age > 18; -- Результат: 101 (фантом!)
COMMIT;
Lost Update (потеря обновления)
Одновременные обновления одного поля перезаписывают друг друга:
Начальный баланс: 1000
-- Транзакция 1
READ balance = 1000
UPDATE balance = 1000 - 100 = 900
-- Транзакция 2
READ balance = 1000
UPDATE balance = 1000 - 50 = 950
-- Результат: 950 вместо 850 (100 потеряны)
УРОВЕНЬ 1: READ UNCOMMITTED (Читать не закоммиченное)
Что решает
Не решает практически ничего - это самый низкий уровень.
Проблемы, которые могут возникнуть
Dirty Read ❌ МОЖЕТ БЫТЬ
Non-Repeatable ❌ МОЖЕТ БЫТЬ
Phantom Read ❌ МОЖЕТ БЫТЬ
Lost Update ❌ МОЖЕТ БЫТЬ
Пример в PostgreSQL
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- Транзакция может читать грязные данные
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- Может быть неверно
COMMIT;
Когда использовать
Никогда в production. Только для отчётов, где точность не критична.
УРОВЕНЬ 2: READ COMMITTED (Читать закоммиченное)
Что решает
Dirty Read ✅ РЕШАЕТ - читаем только закоммиченные данные
Non-Repeatable ❌ МОЖЕТ БЫТЬ
Phantom Read ❌ МОЖЕТ БЫТЬ
Lost Update ❌ МОЖЕТ БЫТЬ
Как работает
-- Транзакция 1
BEGIN;
UPDATE accounts SET balance = 500 WHERE id = 1;
-- Блокируем эту строку
-- Транзакция 2 (одновременно)
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- Ждёт коммита T1
COMMIT;
-- Транзакция 1
COMMIT; -- Теперь T2 может прочитать 500
Проблема: Non-Repeatable Read
@Transactional(isolation = Isolation.READ_COMMITTED)
public void problematicRead() {
// Чтение 1
User user = repository.findById(1);
System.out.println(user.getAge()); // 25
// Другой процесс обновил пользователя
// UPDATE users SET age = 26 WHERE id = 1; COMMIT;
// Чтение 2 (в одной транзакции!)
user = repository.findById(1);
System.out.println(user.getAge()); // 26 (изменилось!)
}
Когда использовать
Это уровень по умолчанию в большинстве БД (MySQL, PostgreSQL). Подходит для большинства приложений.
УРОВЕНЬ 3: REPEATABLE READ (Повторяемое чтение)
Что решает
Dirty Read ✅ РЕШАЕТ
Non-Repeatable ✅ РЕШАЕТ - данные не меняются внутри транзакции
Phantom Read ❌ МОЖЕТ БЫТЬ
Lost Update ❌ МОЖЕТ БЫТЬ (в некоторых БД)
Как работает
-- Транзакция 1
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT age FROM users WHERE id = 1; -- 25
-- Транзакция 2 пытается обновить
UPDATE users SET age = 26 WHERE id = 1;
COMMIT;
-- Транзакция 1 (продолжается)
SELECT age FROM users WHERE id = 1; -- Всё ещё 25!
-- Изменения T2 не видны (блокировка или snapshot)
COMMIT;
Проблема: Phantom Read
-- Транзакция 1
BEGIN REPEATABLE READ;
SELECT COUNT(*) FROM users WHERE age > 18; -- 100
-- Транзакция 2
INSERT INTO users (age) VALUES (25);
COMMIT;
-- Транзакция 1
SELECT COUNT(*) FROM users WHERE age > 18; -- 101 (phantom!)
Java/Hibernate
@Service
public class UserService {
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferMoney() {
User user = repository.findById(1);
double balance = user.getBalance();
// Даже если другой процесс обновит баланс
// Мы в этой транзакции видим старое значение
doSomeThing();
User sameUser = repository.findById(1);
assertEquals(balance, sameUser.getBalance()); // Гарантированно true
}
}
Когда использовать
Когда нужна стабильность данных внутри транзакции (финансовые операции, отчёты).
УРОВЕНЬ 4: SERIALIZABLE (Сериализуемо)
Что решает
Dirty Read ✅ РЕШАЕТ
Non-Repeatable ✅ РЕШАЕТ
Phantom Read ✅ РЕШАЕТ
Lost Update ✅ РЕШАЕТ
Как работает
Транзакции выполняются как будто последовательно (одна за другой):
-- Транзакция 1
BEGIN SERIALIZABLE;
SELECT COUNT(*) FROM users WHERE age > 18; -- 100
-- Транзакция 2 ЖДЁТ
BEGIN SERIALIZABLE;
-- Не может даже начать изменения
-- Транзакция 1
COMMIT; -- Теперь T2 может выполняться
-- Транзакция 2
SELECT COUNT(*) FROM users WHERE age > 18; -- 101 (после T1)
COMMIT;
Стоимость: Deadlock'и
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transfer() {
// Может быть serialization conflict
// PostgreSQL: "could not serialize access"
// MySQL: "Lock wait timeout exceeded"
}
Когда использовать
Очень редко. Только когда абсолютно нужна консистентность и готовы платить производительностью.
Сравнительная таблица
| Уровень | Dirty Read | Non-Rep | Phantom | Lost Update | Производ | Deadlock |
|---|---|---|---|---|---|---|
| READ_UNCOMMITTED | ❌ | ❌ | ❌ | ❌ | ✅ Макс | ✅ Низкий |
| READ_COMMITTED | ✅ | ❌ | ❌ | ❌ | ✅ Хорошо | ✅ Низкий |
| REPEATABLE_READ | ✅ | ✅ | ❌ | ✅ | ⚠️ Медленнее | ⚠️ Средний |
| SERIALIZABLE | ✅ | ✅ | ✅ | ✅ | ❌ Минимум | ❌ Макс |
Практические примеры
1. Финансовая транзакция (высокий уровень)
@Service
public class BankService {
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepo.findByIdForUpdate(fromId);
Account to = accountRepo.findByIdForUpdate(toId);
// SELECT FOR UPDATE блокирует строки
// Гарантирует, что никто не изменит между нашими операциями
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
from.withdraw(amount);
to.deposit(amount);
accountRepo.save(from);
accountRepo.save(to);
}
}
2. Отчёт (низкий уровень)
@Service
public class ReportService {
@Transactional(isolation = Isolation.READ_COMMITTED)
public ReportData generateReport(LocalDate date) {
// Для отчётов точность менее критична
// Немного устаревшие данные - нормально
long totalSales = reportRepo.getTotalSalesForDate(date);
long totalCustomers = reportRepo.getTotalCustomersForDate(date);
return new ReportData(totalSales, totalCustomers);
}
}
3. SELECT FOR UPDATE (явная блокировка)
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.id = :id")
@Lock(LockModeType.PESSIMISTIC_WRITE)
User findByIdWithLock(@Param("id") Long id);
}
// Использование
@Transactional
public void updateUser(Long id) {
User user = repository.findByIdWithLock(id);
// Никто другой не может обновить этого пользователя
user.setName("New Name");
}
Рекомендации по выбору
По типу операции
-
READ_COMMITTED (по умолчанию)
- Веб-приложения
- CRUD операции
- API endpoints
-
REPEATABLE_READ
- Финансовые операции
- Критичные обновления
- Операции с инвентарём
-
SERIALIZABLE
- Аудит данных
- Критичные бизнес-операции
- Готовы к медленности
Вывод
Уровни изоляции - это компромисс между консистентностью и производительностью:
- READ_COMMITTED - стандарт для 90% случаев
- REPEATABLE_READ - когда нужна стабильность данных в транзакции
- Используйте SELECT FOR UPDATE для явной блокировки вместо повышения уровня
- SERIALIZABLE редко нужен - проверьте логику приложения сначала