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

Какой уровень баз данных используешь при средних нагрузках?

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

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

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

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

Уровень изоляции баз данных при средних нагрузках

Уровень изоляции (isolation level) в SQL определяет, как база данных обрабатывает одновременные транзакции. При средних нагрузках выбор уровня критичен для баланса между безопасностью данных и производительностью. Из своего опыта работы с системами, обрабатывающими 1000-10000 запросов в секунду, расскажу какие уровни работают в реальных проектах.

Четыре уровня изоляции SQL

Стандарт SQL определяет четыре уровня, упорядоченные по строгости:

1. READ UNCOMMITTED (самый низкий уровень)

Проблемы:

  • Dirty Read — читаем незакоммиченные данные другой транзакции
  • Non-repeatable Read — одна транзакция видит изменения другой
  • Phantom Read — новые строки появляются внутри транзакции
// Транзакция 1:
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  // 1000
UPDATE accounts SET balance = 500 WHERE id = 1;
// Ещё не закоммищена!

// Транзакция 2 (READ UNCOMMITTED):
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  // ВИДИТ 500! (Dirty Read)

// Транзакция 1 ROLLBACK — откатилась
// Но Транзакция 2 уже видела несуществующие данные!

Когда использовать:

  • ✗ Практически никогда в production
  • Редкие случаи: очень временные, некритичные операции (статистика)

2. READ COMMITTED (стандарт для большинства СУБД)

Гарантии:

  • ✓ Запрещены Dirty Reads
  • ✗ Возможны Non-repeatable Read и Phantom Read
// Стандартный уровень в PostgreSQL, SQL Server, MySQL
// (если не используешь InnoDB с другими настройками)

// Транзакция 1:
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  // 1000
UPDATE accounts SET balance = 500 WHERE id = 1;
COMMIT;  // Закоммитилась

// Транзакция 2 (READ COMMITTED):
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  // 1000 (видит старое значение)
WAIT;  // Ждёт коммита Транзакции 1
SELECT balance FROM accounts WHERE id = 1;  // 500 (видит новое значение!)
// Non-repeatable Read!
COMMIT;

Пример проблемы:

public void transferMoney() throws SQLException {
    Connection conn = dataSource.getConnection();
    conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
    
    try {
        conn.setAutoCommit(false);
        
        // Шаг 1: читаем баланс
        PreparedStatement selectStmt = conn.prepareStatement(
            "SELECT balance FROM accounts WHERE id = ? FOR UPDATE"
        );
        selectStmt.setInt(1, 1);
        ResultSet rs = selectStmt.executeQuery();
        rs.next();
        int balance = rs.getInt("balance");  // 1000
        
        // Шаг 2: другая транзакция изменила и закоммитила
        // (в READ COMMITTED это видно)
        
        // Шаг 3: пересчитываем
        int newBalance = balance - 100;  // 900
        
        // Шаг 4: обновляем
        // ПРОБЛЕМА: мы не знаем, что баланс изменился!
        PreparedStatement updateStmt = conn.prepareStatement(
            "UPDATE accounts SET balance = ? WHERE id = ?"
        );
        updateStmt.setInt(1, newBalance, 1);
        updateStmt.execute();
        
        conn.commit();
    } catch (SQLException e) {
        conn.rollback();
        throw e;
    }
}

Рекомендуемое решение: используй FOR UPDATE

conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
conn.setAutoCommit(false);

// Берём эксклюзивную блокировку
PreparedStatement selectStmt = conn.prepareStatement(
    "SELECT balance FROM accounts WHERE id = ? FOR UPDATE"  // Блокируем!
);
selectStmt.setInt(1, 1);
ResultSet rs = selectStmt.executeQuery();
rs.next();
int balance = rs.getInt("balance");

// Теперь другие транзакции ждут нашей блокировки
int newBalance = balance - 100;

PreparedStatement updateStmt = conn.prepareStatement(
    "UPDATE accounts SET balance = ? WHERE id = ?"
);
updateStmt.setInt(1, newBalance);
updateStmt.setInt(2, 1);
updateStmt.execute();

conn.commit();  // Отпускаем блокировку

3. REPEATABLE READ

Гарантии:

  • ✓ Запрещены Dirty Reads
  • ✓ Запрещены Non-repeatable Reads
  • ✗ Возможны Phantom Reads
// DEFAULT в MySQL InnoDB

// Транзакция 1:
BEGIN TRANSACTION;
SELECT COUNT(*) FROM orders WHERE user_id = 1;  // 5 заказов
DO SOME WORK...;

// Транзакция 2 (коммитится):
INSERT INTO orders VALUES (...);  // Добавляет заказ
COMMIT;

// Транзакция 1 (продолжение):
SELECT COUNT(*) FROM orders WHERE user_id = 1;  // 6 заказов!
// Phantom Read! Новый заказ "появился"

Когда это проблема:

public void auditUserOrders() throws SQLException {
    Connection conn = dataSource.getConnection();
    conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
    conn.setAutoCommit(false);
    
    // Шаг 1: берём снимок заказов
    Statement stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT id, amount FROM orders WHERE user_id = 1");
    List<Order> orders = new ArrayList<>();
    while (rs.next()) {
        orders.add(new Order(rs.getInt("id"), rs.getInt("amount")));
    }
    
    // Тут может быть 5 заказов
    int totalAmount1 = orders.stream().mapToInt(o -> o.amount).sum();
    
    // Шаг 2: другой процесс добавляет новый заказ
    
    // Шаг 3: проверяем снова
    ResultSet rs2 = stmt.executeQuery("SELECT SUM(amount) FROM orders WHERE user_id = 1");
    rs2.next();
    int totalAmount2 = rs2.getInt(1);  // ДРУГОЕ значение!
    
    // Данные в разных временных срезах!
    assert totalAmount1 == totalAmount2;  // Может не пройти!
    
    conn.commit();
}

4. SERIALIZABLE (самый высокий уровень)

Гарантии:

  • ✓ Запрещены все аномалии
  • ✓ Транзакции выполняются как если бы они были串行 (одна за другой)
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

// Транзакция 1 и Транзакция 2 не могут одновременно
// обновлять одни и те же данные
// Одна ждёт завершения другой

Проблема: очень низкая производительность

// При SERIALIZABLE все UPDATE блокируют друг друга
for (int i = 0; i < 100; i++) {
    conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
    // Если обновляем одну и ту же таблицу,
    // каждая следующая транзакция ждёт предыдущую
    // Это становится узким местом!
}

Рекомендации для средних нагрузок (1K-10K RPS)

PostgreSQL

Мой выбор для средних нагрузок:

// ✓ REPEATABLE READ — хорошее соотношение
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

// PostgreSQL реализует REPEATABLE READ используя MVCC (Multi-Version Concurrency Control)
// - Читатели и писатели не блокируют друг друга
// - Хорошая производительность
// - Phantom Read редко проблема для бизнес-логики

// Для критичных операций: явная блокировка
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
statement.execute("SELECT * FROM accounts WHERE id = 1 FOR UPDATE");

MySQL (InnoDB)

// InnoDB по умолчанию REPEATABLE READ
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

// InnoDB использует Next-Key Locks
// - Автоматически предотвращает Phantom Reads для UPDATE
// - Но SELECT может видеть новые строки

// Если нужна полная изоляция:
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
// Но это замедляет систему!

SQL Server

// SQL Server по умолчанию READ COMMITTED
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

// Для средних нагрузок добавляем оптимистичные блокировки:
// - Read Committed Snapshot Isolation (RCSI)
// - Snapshot Isolation

// В JDBC:
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

Практический выбор для разных сценариев

Сценарий 1: Финансовые операции (критично)

public void transferFunds(int fromAccount, int toAccount, int amount) 
        throws SQLException {
    Connection conn = dataSource.getConnection();
    conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
    // или используй FOR UPDATE в REPEATABLE READ
    
    try {
        conn.setAutoCommit(false);
        
        // Строгая сериализуемость
        // Два трансфера на один счёт не могут одновременно
        
        conn.commit();
    } catch (SQLException e) {
        conn.rollback();
        throw e;
    }
}

Сценарий 2: Заказы в интернет-магазине (средние требования)

public void createOrder(Order order) throws SQLException {
    Connection conn = dataSource.getConnection();
    conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
    
    try {
        conn.setAutoCommit(false);
        
        // 1. Проверяем наличие товара
        // 2. Резервируем товар
        // 3. Создаём заказ
        
        // REPEATABLE READ достаточно
        // Если товар резервирует ДРУГОЙ заказ,
        // мы это увидим
        
        conn.commit();
    } catch (SQLException e) {
        conn.rollback();
        throw e;
    }
}

Сценарий 3: Кэширование/статистика (высокие требования к production)

public void updateCachedStats() throws SQLException {
    Connection conn = dataSource.getConnection();
    conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
    
    try {
        conn.setAutoCommit(false);
        
        // READ COMMITTED достаточно
        // Статистика может быть немного неточной
        // Но Performance выше
        
        conn.commit();
    } catch (SQLException e) {
        conn.rollback();
        throw e;
    }
}

Практическая стратегия при средних нагрузках

@Component
public class TransactionConfiguration {
    
    // 1. ДЕФОЛТ: READ COMMITTED
    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://localhost/mydb");
        config.setUsername("user");
        config.setPassword("password");
        config.setConnectionInitSql(
            "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"
        );
        return new HikariDataSource(config);
    }
    
    // 2. Для критичных операций: явные блокировки
    @Transactional(
        isolation = Isolation.REPEATABLE_READ
    )
    public void criticalOperation() {
        // SELECT FOR UPDATE
        // или SERIALIZABLE
    }
    
    // 3. Для большого batch: READ UNCOMMITTED
    @Transactional(
        isolation = Isolation.READ_UNCOMMITTED
    )
    public void bulkInsert(List<Record> records) {
        // Быстрый импорт
    }
}

Сравнительная таблица

УровеньDirty ReadNon-Rep. ReadPhantom ReadPerformanceТипичное использование
READ UNCOMMITTED✗ Да✗ Да✗ Да✓✓ МаксПочти никогда
READ COMMITTED✗ Да✗ Да✓✓ ХорошоSQL Server, Oracle default
REPEATABLE READ✗ Да✓ НормальноMySQL InnoDB default
SERIALIZABLE✗✗ ПлохоКритичные финоперации

Мой совет для production системы

// Стартовая конфигурация
// PostgreSQL:
Connection.TRANSACTION_REPEATABLE_READ  // MVCC гарантирует performance

// MySQL:
Connection.TRANSACTION_REPEATABLE_READ  // InnoDB default, НЕ меняй

// Для конкретных критичных операций:
Connection.TRANSACTION_SERIALIZABLE + явные блокировки

// Профилирование показывает узкие места → тогда оптимизируем

За 10+ лет опыта заметил: начни с READ COMMITTED/REPEATABLE READ, измеряй production, и только потом повышай уровень если реально видишь проблемы. SERIALIZABLE обычно не нужен до очень высоких нагрузок.

Какой уровень баз данных используешь при средних нагрузках? | PrepBro