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

В чем разница между Repeatable Read и Phantom Read?

3.0 Senior🔥 131 комментариев
#Базы данных и SQL

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

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

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

# В чем разница между Repeatable Read и Phantom Read?

Это вопрос о transaction isolation levels в SQL/JDBC. Оба термина описывают проблемы конкурентного доступа к данным, но они решают разные проблемы.

Сначала: Transaction Isolation Levels

SQL стандарт определяет 4 уровня изоляции транзакций (от слабого к сильному):

  1. READ UNCOMMITTED (грязное чтение)
  2. READ COMMITTED (чтение закомиченных данных)
  3. REPEATABLE READ (повторяемое чтение)
  4. 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 UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
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 в современных БД (это помогает)
В чем разница между Repeatable Read и Phantom Read? | PrepBro