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

Всегда ли будет запрет параллельного чтения при чтении одной записи транзакциями из разных потоков?

3.0 Senior🔥 91 комментариев
#Базы данных и SQL#Многопоточность

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

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

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

Параллельное чтение в транзакциях из разных потоков

Этот вопрос касается уровней изоляции транзакций (ACID) и того, может ли одновременно несколько потоков читать одну и ту же запись. Ответ: НЕТ, не всегда будет запрет. Это зависит от уровня изоляции.

Уровни изоляции транзакций (ISO/IEC)

В SQL стандарте определены 4 уровня изоляции, от самого слабого к самому строгому:

1. READ UNCOMMITTED (самый низкий)

Означает: транзакция может читать незакомиченные изменения (Dirty Read).

// Поток 1
Connection conn1 = getConnection();
conn1.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
conn1.setAutoCommit(false);

Statement stmt1 = conn1.createStatement();
ResultSet rs1 = stmt1.executeQuery("SELECT balance FROM account WHERE id = 1");
rs1.next();
int balance1 = rs1.getInt("balance");  // Может быть незакомиченное значение!
conn1.commit();

// Поток 2 в это же время
Connection conn2 = getConnection();
conn2.setAutoCommit(false);
Statement stmt2 = conn2.createStatement();
stmt2.executeUpdate("UPDATE account SET balance = 5000 WHERE id = 1");
// НЕ коммитим! (Rollback может быть позже)

// Поток 1 видит новое значение (5000) хотя оно не закомичено
// Если Поток 2 откатится — Поток 1 видел "грязное" значение (Dirty Read)

Характеристика: ПАРАЛЛЕЛЬНОЕ ЧТЕНИЕ РАЗРЕШЕНО

2. READ COMMITTED (уровень по умолчанию в большинстве БД)

Означает: читаются только закомиченные данные, но возможны фантомные чтения.

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

// Поток 1
Statement stmt1 = conn.createStatement();
int balance1 = 1000;
ResultSet rs1 = stmt1.executeQuery("SELECT balance FROM account WHERE id = 1");
rs1.next();
balance1 = rs1.getInt("balance");  // Может быть закомиченное изменение
// Пауза 2 секунды
rs1 = stmt1.executeQuery("SELECT balance FROM account WHERE id = 1");
rs1.next();
int balance2 = rs1.getInt("balance");  // Может быть ДРУГОЙ результат!
// Это Non-repeatable Read

conn.commit();

ПАРАЛЛЕЛЬНОЕ ЧТЕНИЕ РАЗРЕШЕНО, но видны только закомиченные изменения.

3. REPEATABLE READ

Означает: одна транзакция видит консистентный снимок данных на момент начала. Но возможны фантомные строки.

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

// Поток 1
Statement stmt = conn.createStatement();
int balance1 = 1000;
ResultSet rs = stmt.executeQuery("SELECT balance FROM account WHERE id = 1");
rs.next();
balance1 = rs.getInt("balance");  // Например: 1000

// В это время Поток 2 изменяет и коммитит новое значение
Thread.sleep(2000);  // Пауза

// Но Поток 1 ВСЕГДА видит 1000 (снимок начала транзакции)
rs = stmt.executeQuery("SELECT balance FROM account WHERE id = 1");
rs.next();
int balance2 = rs.getInt("balance");  // Всё ещё 1000

conn.commit();

ПАРАЛЛЕЛЬНОЕ ЧТЕНИЕ РАЗРЕШЕНО, но каждая транзакция видит снимок.

4. SERIALIZABLE (самый высокий)

Означает: полная изоляция, как если бы транзакции выполнялись последовательно.

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

// Поток 1
Statement stmt = conn.createStatement();
int balance = 1000;
ResultSet rs = stmt.executeQuery("SELECT balance FROM account WHERE id = 1");
rs.next();
balance = rs.getInt("balance");

// Поток 2 пытается это же прочитать — может быть заблокирован!
Connection conn2 = getConnection();
conn2.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
conn2.setAutoCommit(false);

Statement stmt2 = conn2.createStatement();
int balance2 = 1000;
// Может ждать, пока Поток 1 закончит транзакцию
ResultSet rs2 = stmt2.executeQuery("SELECT balance FROM account WHERE id = 1");

ПАРАЛЛЕЛЬНОЕ ЧТЕНИЕ МОЖЕТ БЫТЬ ЗАПРЕЩЕНО (или сериализовано).

Таблица: явления и уровни изоляции

УровеньDirty ReadNon-repeatable ReadPhantom Read
READ_UNCOMMITTEDДаДаДа
READ_COMMITTEDНетДаДа
REPEATABLE_READНетНетДа (или Нет в некоторых БД)
SERIALIZABLEНетНетНет

Практический пример на PostgreSQL

public class TransactionIsolationExample {
    public static void main(String[] args) throws Exception {
        // Настройка БД: PostgreSQL
        String url = "jdbc:postgresql://localhost:5432/mydb";
        
        // Поток 1: READ COMMITTED
        Thread t1 = new Thread(() -> {
            try {
                Connection conn = DriverManager.getConnection(url, "user", "pass");
                conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
                conn.setAutoCommit(false);
                
                for (int i = 0; i < 3; i++) {
                    Statement stmt = conn.createStatement();
                    ResultSet rs = stmt.executeQuery("SELECT balance FROM account WHERE id = 1");
                    rs.next();
                    System.out.println("[Поток 1] Попытка " + i + ": " + rs.getInt("balance"));
                    
                    Thread.sleep(1000);
                }
                
                conn.commit();
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        
        // Поток 2: изменяет и коммитит
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(500);  // Начинает позже
                
                Connection conn = DriverManager.getConnection(url, "user", "pass");
                conn.setAutoCommit(false);
                
                Statement stmt = conn.createStatement();
                stmt.executeUpdate("UPDATE account SET balance = 2000 WHERE id = 1");
                conn.commit();
                System.out.println("[Поток 2] Изменили и закомитили на 2000");
                
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

// Вывод:
// [Поток 1] Попытка 0: 1000
// [Поток 2] Изменили и закомитили на 2000
// [Поток 1] Попытка 1: 2000  <- Видит новое значение!
// [Поток 1] Попытка 2: 2000

Блокировки на уровне БД

Когда транзакция читает данные, БД может установить блокировки:

public class DatabaseLocks {
    public static void main(String[] args) throws Exception {
        Connection conn = DriverManager.getConnection("jdbc:mysql://...");
        
        // SELECT ... FOR UPDATE (явная блокировка)
        // Поток 1 
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(
            "SELECT balance FROM account WHERE id = 1 FOR UPDATE"
        );
        rs.next();
        int balance = rs.getInt("balance");
        
        // Поток 2 попытается читать эту же строку
        // В зависимости от уровня изоляции:
        // - Может прочитать (READ_COMMITTED)
        // - Может ждать (SERIALIZABLE)
        // - Может получить снимок (REPEATABLE_READ)
    }
}

Реальная сценарий: банковская система

public class BankTransactionExample {
    public static void transferMoney(int fromId, int toId, double amount) throws SQLException {
        Connection conn = DriverManager.getConnection("jdbc:mysql://...");
        
        try {
            // SERIALIZABLE для критичных операций
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
            conn.setAutoCommit(false);
            
            // 1. Читаем баланс отправителя
            Statement stmt1 = conn.createStatement();
            ResultSet rs1 = stmt1.executeQuery(
                "SELECT balance FROM account WHERE id = " + fromId
            );
            rs1.next();
            double fromBalance = rs1.getDouble("balance");
            
            if (fromBalance < amount) {
                throw new SQLException("Недостаточно средств");
            }
            
            Thread.sleep(100);  // Имитация обработки
            
            // 2. Читаем баланс получателя
            Statement stmt2 = conn.createStatement();
            ResultSet rs2 = stmt2.executeQuery(
                "SELECT balance FROM account WHERE id = " + toId
            );
            rs2.next();
            double toBalance = rs2.getDouble("balance");
            
            // 3. Обновляем оба счёта
            Statement stmt3 = conn.createStatement();
            stmt3.executeUpdate(
                "UPDATE account SET balance = balance - " + amount +
                " WHERE id = " + fromId
            );
            
            Statement stmt4 = conn.createStatement();
            stmt4.executeUpdate(
                "UPDATE account SET balance = balance + " + amount +
                " WHERE id = " + toId
            );
            
            conn.commit();
            
        } catch (Exception e) {
            conn.rollback();
            throw new SQLException(e);
        } finally {
            conn.close();
        }
    }
}

Ответ на вопрос

НЕТ, не всегда будет запрет параллельного чтения.

Это зависит от уровня изоляции:

  • READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ: параллельное чтение разрешено
  • SERIALIZABLE: может быть запрещено или заблокировано

Дополнительно:

  • По умолчанию: большинство БД используют READ COMMITTED
  • Для критичных операций: используйте SERIALIZABLE
  • Для оптимизации: помните, что повышение уровня изоляции снижает параллелизм
  • В Java: используйте setTransactionIsolation() для явного указания уровня

Выбор уровня изоляции — это балансировка между консистентностью данных и производительностью.