← Назад к вопросам
В чем разница между фантомным чтением и неповторяющимся чтением?
1.2 Junior🔥 131 комментариев
#Основы Java
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Фантомное чтение vs Неповторяющееся чтение в SQL
Введение
Это две разные проблемы параллельности в базах данных, возникающие при определённых уровнях изоляции транзакций. Понимание различия критично для выбора правильного уровня изоляции.
Уровни изоляции транзакций
Java определяет 4 уровня (java.sql.Connection):
// От наименьшей изоляции к наибольшей
Connection.TRANSACTION_READ_UNCOMMITTED; // 0
Connection.TRANSACTION_READ_COMMITTED; // 1
Connection.TRANSACTION_REPEATABLE_READ; // 2
Connection.TRANSACTION_SERIALIZABLE; // 4
Неповторяющееся чтение (Non-Repeatable Read)
Неповторяющееся чтение — это когда одна и та же строка при повторном чтении имеет другое значение в рамках одной транзакции.
Сценарий проблемы
Транзакция 1 (T1) Транзакция 2 (T2)
================ ================
BEGIN
SELECT price FROM products BEGIN
WHERE id = 1 SELECT price FROM products
→ price = 100 WHERE id = 1
→ price = 100
UPDATE products
SET price = 150
WHERE id = 1
COMMIT
SELECT price FROM products
WHERE id = 1
→ price = 150 ← CHANGED!
COMMIT
Проблема в коде Java
public void nonRepeatableReadExample() throws SQLException {
Connection conn = DriverManager.getConnection(url, user, pass);
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
try {
conn.setAutoCommit(false);
// Первое чтение
Statement stmt1 = conn.createStatement();
ResultSet rs1 = stmt1.executeQuery("SELECT price FROM products WHERE id = 1");
rs1.next();
int price1 = rs1.getInt("price"); // price1 = 100
System.out.println("First read: " + price1);
// В этот момент другая транзакция может изменить строку
Thread.sleep(2000);
// Второе чтение
Statement stmt2 = conn.createStatement();
ResultSet rs2 = stmt2.executeQuery("SELECT price FROM products WHERE id = 1");
rs2.next();
int price2 = rs2.getInt("price"); // price2 = 150 (ИЗМЕНИЛОСЬ!)
System.out.println("Second read: " + price2);
if (price1 != price2) {
System.out.println("NON-REPEATABLE READ!");
}
conn.commit();
} catch (Exception e) {
conn.rollback();
}
}
Фантомное чтение (Phantom Read)
Фантомное чтение — это когда результаты запроса изменяются между двумя запросами в рамках одной транзакции из-за добавления или удаления строк другой транзакцией.
Сценарий проблемы
Транзакция 1 (T1) Транзакция 2 (T2)
================ ================
BEGIN
SELECT COUNT(*) FROM accounts BEGIN
WHERE balance > 1000
→ count = 5 INSERT INTO accounts
(name, balance)
VALUES ('New User', 1500)
COMMIT
SELECT COUNT(*) FROM accounts
WHERE balance > 1000
→ count = 6 ← PHANTOM ROW!
COMMIT
Проблема в коде Java
public void phantomReadExample() throws SQLException {
Connection conn = DriverManager.getConnection(url, user, pass);
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
try {
conn.setAutoCommit(false);
// Первый запрос
Statement stmt1 = conn.createStatement();
ResultSet rs1 = stmt1.executeQuery(
"SELECT id, name FROM accounts WHERE balance > 1000"
);
int count1 = 0;
while (rs1.next()) {
count1++;
System.out.println("Account: " + rs1.getString("name"));
}
System.out.println("First count: " + count1); // count1 = 5
// В этот момент другая транзакция может добавить/удалить строки
Thread.sleep(2000);
// Второй запрос с теми же условиями
Statement stmt2 = conn.createStatement();
ResultSet rs2 = stmt2.executeQuery(
"SELECT id, name FROM accounts WHERE balance > 1000"
);
int count2 = 0;
while (rs2.next()) {
count2++;
System.out.println("Account: " + rs2.getString("name"));
}
System.out.println("Second count: " + count2); // count2 = 6 (ИЗМЕНИЛОСЬ!)
if (count1 != count2) {
System.out.println("PHANTOM READ!");
}
conn.commit();
} catch (Exception e) {
conn.rollback();
}
}
Ключевые различия
Таблица сравнения
| Аспект | Неповторяющееся чтение | Фантомное чтение |
|---|---|---|
| Что изменяется | Существующая строка (UPDATE) | Новые/удалённые строки (INSERT/DELETE) |
| Тип операции | UPDATE | INSERT / DELETE |
| Количество результатов | Остаётся прежним | Изменяется |
| Значения в строках | Меняются | Новые/исчезают |
| Пример | Цена товара изменилась | Появился новый товар |
| Решение | REPEATABLE_READ | SERIALIZABLE |
На уровне SQL
-- Неповторяющееся чтение: UPDATE
BEGIN;
SELECT price FROM products WHERE id = 1; -- 100
-- Другая транзакция: UPDATE products SET price = 150 WHERE id = 1;
SELECT price FROM products WHERE id = 1; -- 150 (CHANGED)
COMMIT;
-- Фантомное чтение: INSERT
BEGIN;
SELECT COUNT(*) FROM products WHERE price > 100; -- 5
-- Другая транзакция: INSERT INTO products VALUES (...);
SELECT COUNT(*) FROM products WHERE price > 100; -- 6 (NEW ROW)
COMMIT;
Матрица изоляции
| Уровень | Dirty Read | Non-Repeatable Read | Phantom Read |
|---|---|---|---|
| READ_UNCOMMITTED | ✗ | ✗ | ✗ |
| READ_COMMITTED | ✓ | ✗ | ✗ |
| REPEATABLE_READ | ✓ | ✓ | ✗ |
| SERIALIZABLE | ✓ | ✓ | ✓ |
(✓ = проблема отсутствует, ✗ = возможна проблема)
Практический пример: Spring/Hibernate
@Service
public class AccountService {
@Transactional(
isolation = Isolation.READ_COMMITTED
)
public void checkBalance() {
// Может быть non-repeatable read
Account account = accountRepository.findById(1L);
System.out.println("Balance: " + account.getBalance());
// ... другой код ...
// Balance может измениться!
account = accountRepository.findById(1L);
}
@Transactional(
isolation = Isolation.REPEATABLE_READ
)
public void processAccounts() {
// Не будет non-repeatable read
// Но возможно phantom read
List<Account> accounts = accountRepository.findByBalanceGreaterThan(1000);
System.out.println("Count: " + accounts.size());
// ... другой код ...
// Количество может измениться!
accounts = accountRepository.findByBalanceGreaterThan(1000);
}
@Transactional(
isolation = Isolation.SERIALIZABLE
)
public void criticalOperation() {
// Полная сериализуемость
// Ни non-repeatable read, ни phantom read
// Но медленнее!
}
}
Рекомендации
Выбор уровня изоляции
// 1. Большинство случаев — READ_COMMITTED (по умолчанию)
// Хороший баланс между производительностью и корректностью
@Transactional
public void normalOperation() { }
// 2. Если нужны последовательные чтения одних данных
// REPEATABLE_READ
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void consistentReads() { }
// 3. Только для критичных операций — SERIALIZABLE
// Снижает concurrent throughput
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalFinancialOperation() { }
Заключение
- Неповторяющееся чтение: один и тот же РЯД имеет разные значения (UPDATE)
- Фантомное чтение: результаты запроса меняются из-за новых/удалённых рядов (INSERT/DELETE)
- READ_COMMITTED защищает от обоих
- REPEATABLE_READ защищает от non-repeatable, но не от phantom
- SERIALIZABLE защищает от всех проблем, но медленнее
- Используй правильный уровень изоляции для твоего случая использования