В чем разница между Repeatable Read и Phantom Read?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# В чем разница между Repeatable Read и Phantom Read?
Это вопрос о transaction isolation levels в SQL/JDBC. Оба термина описывают проблемы конкурентного доступа к данным, но они решают разные проблемы.
Сначала: Transaction Isolation Levels
SQL стандарт определяет 4 уровня изоляции транзакций (от слабого к сильному):
- READ UNCOMMITTED (грязное чтение)
- READ COMMITTED (чтение закомиченных данных)
- REPEATABLE READ (повторяемое чтение)
- SERIALIZABLE (сериализуемо)
Repeatable Read — гарантирует одинаковые данные
РEPEATABLE READ предотвращает "Non-Repeatable Read".
Проблема: Non-Repeatable Read
Транзакция 1 | Транзакция 2
-----------------------------|---
BEGIN; |
SELECT * FROM users |
WHERE id = 1; |
→ name = "Alice", age = 25 |
| UPDATE users SET age = 26 WHERE id = 1;
| COMMIT;
SELECT * FROM users |
WHERE id = 1; |
→ name = "Alice", age = 26 | ❌ ДРУГОЕ значение!
|
COMMIT; |
Транзакция 1 прочитала один и тот же объект дважды и получила РАЗНЫЕ значения. Это Non-Repeatable Read.
Решение: REPEATABLE READ
Транзакция 1 (REPEATABLE READ) | Транзакция 2
-----------------------------|---
BEGIN; |
SELECT * FROM users |
WHERE id = 1; |
→ name = "Alice", age = 25 | LOCK применяется!
| UPDATE users SET age = 26 WHERE id = 1;
| ❌ БЛОКИРОВАНО (waiting...)
SELECT * FROM users |
WHERE id = 1; |
→ name = "Alice", age = 25 | ✅ ТО ЖЕ значение!
COMMIT; |
| UPDATE users SET age = 26 WHERE id = 1;
| ✅ Теперь работает
С REPEATABLE READ гарантирует: если ты прочитал данные, то в этой транзакции они не изменятся.
Phantom Read — новые строки появляются
Phantom Read — это совсем другое. Это когда НОВЫЕ строки появляются, удовлетворяющие WHERE условию.
Проблема: Phantom Read
Транзакция 1 | Транзакция 2
-----------------------------------------|---
BEGIN; |
SELECT COUNT(*) FROM orders |
WHERE status = "COMPLETED" |
→ 5 orders |
Читаем первый раз |
| INSERT INTO orders VALUES(..., "COMPLETED");
| INSERT INTO orders VALUES(..., "COMPLETED");
| COMMIT;
SELECT COUNT(*) FROM orders |
WHERE status = "COMPLETED" |
→ 7 orders | ❌ НЕ ТЕ ЖЕ СТРОКИ!
Счет изменился! |
COMMIT; |
Транзакция 1 выполнила один и тот же SELECT дважды, но получила ДРУГОЕ количество строк. Это Phantom Read.
Отличие от Non-Repeatable Read:
- Non-Repeatable Read: СУЩЕСТВУЮЩИЕ строки меняют ЗНАЧЕНИЯ
- Phantom Read: НОВЫЕ строки появляются или исчезают
Зачем это важно для разработчиков
// Пример 1: Non-Repeatable Read (Repeatable Read уровень решает)
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void updateUserAge(Long userId) {
User user = userRepository.findById(userId).get();
System.out.println("Age: " + user.getAge()); // 25
// Другой процесс обновляет возраст
// user.setAge(26);
User user2 = userRepository.findById(userId).get();
System.out.println("Age: " + user2.getAge()); // REPEATABLE READ: 25
// БЕЗ REPEATABLE READ: 26
}
// Пример 2: Phantom Read (даже SERIALIZABLE может быть проблема)
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void countOrders(String status) {
Long count1 = orderRepository.countByStatus(status);
System.out.println("Count 1: " + count1); // 5
// Другой процесс добавляет новые orders
// insert into orders values (..., "COMPLETED");
Long count2 = orderRepository.countByStatus(status);
System.out.println("Count 2: " + count2); // REPEATABLE READ: может быть 7
// REPEATABLE READ НЕ защищает от phantom read!
}
Таблица: что защищает от чего
| Проблема | READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE |
|---|---|---|---|---|
| Dirty Read | НЕТ | ДА | ДА | ДА |
| Non-Repeatable Read | НЕТ | НЕТ | ДА | ДА |
| Phantom Read | НЕТ | НЕТ | НЕТ | ДА |
Зачем REPEATABLE READ существует, если он не защищает от Phantom Read?
1. Phantom Read редкий в реальной жизни
2. SERIALIZABLE медленный (блокирует весь диапазон строк)
3. REPEATABLE READ дает хороший баланс:
- Защищает от Non-Repeatable Read
- Достаточно быстрый
- Подходит для большинства случаев
Практические примеры в Java
Пример 1: Счет платежей
@Service
public class PaymentService {
// ❌ Без Repeatable Read: риск Non-Repeatable Read
@Transactional(isolation = Isolation.READ_COMMITTED)
public void processPayment(Long userId) {
// Сначала проверяем баланс
BigDecimal balance1 = accountRepository.getBalance(userId); // 1000
// Другой процесс может уменьшить баланс
// UPDATE accounts SET balance = 500 WHERE user_id = ?
// Проверяем баланс снова
BigDecimal balance2 = accountRepository.getBalance(userId); // 500
// Несогласованность!
}
// ✅ С Repeatable Read: гарантирует консистентность
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void processPaymentSafely(Long userId) {
BigDecimal balance1 = accountRepository.getBalance(userId); // 1000
// Другой процесс ждет завершения этой транзакции
// UPDATE accounts SET balance = 500 WHERE user_id = ?
// ← WAITING...
BigDecimal balance2 = accountRepository.getBalance(userId); // 1000 (ГАРАНТИРОВАНО)
// Консистентность соблюдена!
}
}
Пример 2: Инвентарь товаров
@Service
public class InventoryService {
// Phantom Read может быть проблемой
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void shipOrders(String status) {
// Узнаем сколько заказов нужно отправить
List<Order> orders1 = orderRepository.findByStatus(status);
System.out.println("Found " + orders1.size() + " orders"); // 5
// Начинаем обрабатывать
for (Order order : orders1) {
process(order);
}
// Проверяем, остались ли еще
List<Order> orders2 = orderRepository.findByStatus(status);
System.out.println("Remaining " + orders2.size() + " orders"); // Может быть 3 или 7!
// PHANTOM READ: новые заказы могли быть добавлены другим процессом
}
// Решение: использовать SERIALIZABLE если Phantom Read критичен
@Transactional(isolation = Isolation.SERIALIZABLE)
public void shipOrdersSafely(String status) {
List<Order> orders = orderRepository.findByStatus(status);
// Гарантировано: новые orders не будут добавлены в этот статус
// пока эта транзакция не завершится
}
}
MVCC (Multi-Version Concurrency Control) — как это работает
Современные БД (PostgreSQL, MySQL с InnoDB) используют MVCC, что меняет поведение isolation levels.
PostgreSQL: REPEATABLE READ
Транзакция 1 | Транзакция 2
---------------------------------|---
BEGIN ISOLATION LEVEL REPEATABLE READ;
| BEGIN;
SELECT * FROM users WHERE id = 1 |
→ Снимок (snapshot) от момента |
начала транзакции |
| UPDATE users SET age = 26 WHERE id = 1;
| COMMIT;
SELECT * FROM users WHERE id = 1 |
Использует ТОТ ЖЕ снимок |
→ Все еще видит старые данные |
(Non-Repeatable Read защищен!) |
|
NO UPDATES TO NEW ROWS INSERTED | INSERT INTO users VALUES (...);
Все еще не видит новые строки | COMMIT;
(Phantom Read НЕ происходит!) |
COMMIT; |
Рекомендации для разработчиков
Используй REPEATABLE READ когда:
- Нужна гарантия, что данные не меняются внутри транзакции
- Чтение одних и тех же данных несколько раз
- Не критичны новые строки (Phantom Read)
- Нужна хорошая производительность
@Transactional(isolation = Isolation.REPEATABLE_READ)
public OrderDTO prepareOrder(Long orderId) {
// Сначала проверяем
Order order = orderRepository.findById(orderId).get();
if (order.getStatus().equals("PROCESSING")) {
// Потом снова читаем
order = orderRepository.findById(orderId).get();
// REPEATABLE READ гарантирует: status БУДЕТ "PROCESSING"
}
}
Используй SERIALIZABLE когда:
- Phantom Read критичен
- Нужна полная изоляция
- Производительность не критична
@Transactional(isolation = Isolation.SERIALIZABLE)
public void deductInventory(Long productId, int quantity) {
Product product = productRepository.findById(productId).get();
// Гарантировано: никто не добавит или не удалит похожие товары
if (product.getStock() >= quantity) {
product.setStock(product.getStock() - quantity);
productRepository.save(product);
}
}
По умолчанию используй READ_COMMITTED:
// Spring по умолчанию
@Transactional // = Isolation.DEFAULT (обычно READ_COMMITTED)
public void simpleRead() {
// Для большинства операций достаточно
}
Резюме
REPEATABLE READ:
- Защищает от Non-Repeatable Read
- Гарантирует: если ты прочитал строку, она не изменится
- Не защищает от Phantom Read (новые строки могут появиться)
Phantom Read:
- Новые строки появляются/исчезают в результате запроса
- Не решается REPEATABLE READ
- Требует SERIALIZABLE (дорого по производительности)
Практика:
- Используй REPEATABLE READ для большинства случаев
- Используй SERIALIZABLE только если действительно нужно
- Помни о MVCC в современных БД (это помогает)