Что такое уровень изоляции Read Committed?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Уровень изоляции Read Committed (READ_COMMITTED)
Read Committed — это уровень изоляции транзакций в реляционных базах данных (согласно стандарту ANSI SQL). Это второй по строгости уровень изоляции, который обеспечивает компромисс между согласованностью данных и производительностью. При этом уровне транзакция может читать только те данные, которые были зафиксированы (committed) другими транзакциями, но остается уязвимой к некоторым аномалиям.
Четыре уровня изоляции транзакций
- Read Uncommitted (наименее строгий) — может читать грязные данные
- Read Committed — читает только зафиксированные данные
- Repeatable Read — запрещает dirty reads и non-repeatable reads
- Serializable (наиболее строгий) — полная изоляция
Гарантии Read Committed
✅ ЧТО РАЗРЕШАЕТ Read Committed
// Читать только зафиксированные (committed) данные
// Избегать "грязного чтения" (dirty reads)
❌ ЧТО НЕ РАЗРЕШАЕТ Read Committed
// Защита от non-repeatable reads (phantom reads)
// Защита от lost updates (потерь обновлений)
Проблема: Dirty Reads (РЕШЕНА)
Dirty Read — это чтение данных, которые еще не зафиксированы другой транзакцией.
-- Транзакция 1
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- balance изменился с 1000 на 900, но транзакция еще открыта
-- Транзакция 2 (Read Uncommitted уровень)
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- ГРЯЗНОЕ ЧТЕНИЕ: 900
COMMIT;
-- Транзакция 1
ROLLBACK; -- Откатывают изменение
-- Теперь транзакция 2 прочитала несуществующее значение!
-- Read Committed ПРЕДОТВРАЩАЕТ это
-- Транзакция 2 не может прочитать 900, пока транзакция 1 не зафиксирует изменение
Проблема: Non-Repeatable Reads (НЕ РЕШЕНА)
Non-Repeatable Read — это когда одна транзакция читает одно значение, а потом читает его снова и получает другой результат.
-- Транзакция 1
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- Результат: 1000
-- (пока транзакция 1 открыта)
-- Транзакция 2 (изменяет данные)
BEGIN TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT; -- Зафиксирует изменение
-- Транзакция 1
SELECT balance FROM accounts WHERE id = 1; -- Результат: 900 (изменился!)
COMMIT;
-- Проблема: в одной транзакции мы прочитали разные значения одного поля
-- Read Committed НЕ защищает от этого!
Проблема: Phantom Reads (НЕ РЕШЕНА)
Phantom Read — это когда строки, которые соответствуют условию WHERE, появляются или исчезают между двумя чтениями в одной транзакции.
-- Транзакция 1
BEGIN TRANSACTION;
SELECT COUNT(*) FROM employees WHERE salary > 50000;
-- Результат: 5 сотрудников
-- Транзакция 2
BEGIN TRANSACTION;
INSERT INTO employees VALUES (6, 'John', 60000);
COMMIT;
-- Транзакция 1
SELECT COUNT(*) FROM employees WHERE salary > 50000;
-- Результат: 6 сотрудников (появился новый!)
COMMIT;
-- Проблема: набор строк изменился между двумя SELECT
// Read Committed НЕ защищает от этого!
Пример: Потеря обновлений (Lost Update)
-- Сценарий: Двое агентов обновляют баланс счета одновременно
-- Начальное значение: balance = 1000
-- Транзакция 1 (Агент 1)
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- Результат: 1000
UPDATE accounts SET balance = balance + 500 WHERE id = 1; -- balance = 1500
COMMIT;
-- Транзакция 2 (Агент 2) выполняется параллельно
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- Результат: 1000 (еще не видит обновление)
UPDATE accounts SET balance = balance + 200 WHERE id = 1; -- Вычисляет 1000 + 200 = 1200
COMMIT; -- Зафиксирует balance = 1200
-- Финальный результат: balance = 1200
// ПРОБЛЕМА: Потеряно добавление 500!
// Должно было быть: 1000 + 500 + 200 = 1700
Как избежать Lost Updates при Read Committed
Решение 1: SELECT FOR UPDATE
-- Транзакция 1
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- Блокирует строку
-- Другие транзакции не могут менять эту строку!
UPDATE accounts SET balance = balance + 500 WHERE id = 1;
COMMIT;
-- Транзакция 2 должна ждать, пока транзакция 1 завершится
Решение 2: Версионирование (Optimistic Locking)
// Таблица с версией
// CREATE TABLE accounts (
// id INT PRIMARY KEY,
// balance DECIMAL,
// version INT
// );
BEGIN TRANSACTION;
-- Получаем текущую версию
SELECT balance, version FROM accounts WHERE id = 1; -- balance = 1000, version = 5
-- Обновляем и увеличиваем версию
UPDATE accounts
SET balance = balance + 500, version = version + 1
WHERE id = 1 AND version = 5; -- Проверяем версию!
-- Если версия изменилась (другая транзакция обновила), UPDATE не выполнится
COMMIT;
Решение 3: Pessimistic Locking
// Используя JPA
@Entity
@Table(name = "accounts")
public class Account {
@Id
private Long id;
private BigDecimal balance;
@Version
private Long version;
}
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE) // Пессимистическая блокировка
@Query("SELECT a FROM Account a WHERE a.id = :id")
Optional<Account> findByIdWithLock(@Param("id") Long id);
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transferMoney(Long accountId, BigDecimal amount) {
Account account = accountRepository.findByIdWithLock(accountId).get();
account.setBalance(account.getBalance().add(amount));
accountRepository.save(account);
}
Пример использования в Java
import java.sql.*;
public class ReadCommittedExample {
public static void main(String[] args) throws Exception {
Connection conn = DriverManager.getConnection(
"jdbc:postgresql://localhost:5432/mydb",
"user",
"password"
);
// Установить уровень изоляции
conn.setTransactionIsolation(
Connection.TRANSACTION_READ_COMMITTED
);
conn.setAutoCommit(false);
try {
PreparedStatement stmt = conn.prepareStatement(
"SELECT balance FROM accounts WHERE id = ? FOR UPDATE"
);
stmt.setInt(1, 1);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
BigDecimal balance = rs.getBigDecimal("balance");
BigDecimal newBalance = balance.add(BigDecimal.valueOf(500));
PreparedStatement updateStmt = conn.prepareStatement(
"UPDATE accounts SET balance = ? WHERE id = ?"
);
updateStmt.setBigDecimal(1, newBalance);
updateStmt.setInt(2, 1);
updateStmt.executeUpdate();
}
conn.commit(); // Зафиксировать транзакцию
} catch (SQLException e) {
conn.rollback(); // Откатить при ошибке
e.printStackTrace();
} finally {
conn.close();
}
}
}
Сравнение уровней изоляции
┌──────────────────────┬──────────────────┬──────────────┬─────────────┐
│ Уровень изоляции │ Dirty Read │ Non-Rep Read │ Phantom Read│
├──────────────────────┼──────────────────┼──────────────┼─────────────┤
│ Read Uncommitted │ ✅ Возможен │ ✅ Возможен │ ✅ Возможен │
│ Read Committed │ ❌ Невозможен │ ✅ Возможен │ ✅ Возможен │
│ Repeatable Read │ ❌ Невозможен │ ❌ Невозможен│ ✅ Возможен │
│ Serializable │ ❌ Невозможен │ ❌ Невозможен│ ❌ Невозможен│
└──────────────────────┴──────────────────┴──────────────┴─────────────┘
Когда использовать Read Committed
✅ ИСПОЛЬЗУЙ Read Committed когда:
- Нужна хорошая производительность
- Приложение может обрабатывать phantom reads
- Большинство операций — это чтение
- OLTP приложения (Online Transaction Processing)
❌ НЕ ИСПОЛЬЗУЙ Read Committed когда:
- Критична абсолютная согласованность
- Часто бывают conflicting updates
- Нужна серийная обработка транзакций
- Аналитические запросы требуют стабильности (OLAP)
Best Practices
- Понимай уровень изоляции своей БД
- Используй SELECT FOR UPDATE для критичных данных
- Реализуй оптимистическое блокирование (@Version в JPA)
- Минимизируй время открытой транзакции
- Тестируй race conditions в многопоточной среде
- Документируй конфликтные операции
Read Committed — это стандартный уровень изоляции в большинстве современных БД (PostgreSQL, MySQL, Oracle). Он предлагает разумный баланс между производительностью и надежностью, но требует внимательного управления при работе с конфликтующими обновлениями.