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

Как Serializable выбирает, какая транзакция попадет в базу данных

2.0 Middle🔥 201 комментариев
#Основы Java

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

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

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

# Serializable уровень изоляции транзакций

Этот вопрос касается уровня изоляции транзакций Serializable в контексте ACID-гарантий и механики работы СУБД. Важно понимать, что Serializable не выбирает, какая транзакция попадёт в БД — все должны попасть. Вместо этого он гарантирует, что транзакции выполняются так, как если бы они выполнялись последовательно (serialization), несмотря на параллельное выполнение.

Что такое уровни изоляции транзакций

В SQL (и Java JDBC) есть 4 стандартных уровня изоляции транзакций:

  1. READ UNCOMMITTED — самый низкий, может читать незафиксированные (dirty reads) изменения других транзакций
  2. READ COMMITTED — не читаем незафиксированные изменения (стандарт для большинства БД)
  3. REPEATABLE READ — дополнительно гарантирует стабильность прочитанных данных внутри одной транзакции
  4. SERIALIZABLE — самый строгий, полная изоляция, как если бы транзакции выполнялись последовательно

Serializable: Как это работает

Connection conn = DriverManager.getConnection(url);
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
conn.setAutoCommit(false);

try {
    // Транзакция 1
    Statement stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT balance FROM accounts WHERE id = 1");
    int balance = rs.getInt("balance");
    
    // Какая-то бизнес-логика
    Thread.sleep(1000); // Имитация долгой работы
    
    // Обновление
    stmt.executeUpdate("UPDATE accounts SET balance = " + (balance + 100) + " WHERE id = 1");
    
    conn.commit(); // Транзакция успешно завершена
} catch (Exception e) {
    conn.rollback(); // Откат при ошибке
}

Механизм обеспечения Serializable

Пригроваты СУБД используют несколько подходов:

1. Пессимистичное блокирование (Locking)

Большинство СУБД (PostgreSQL, MySQL, Oracle) при Serializable уровне используют блокировки:

// Транзакция 1 (первая начинает)
conn1.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
conn1.setAutoCommit(false);
Statement stmt1 = conn1.createStatement();
ResultSet rs1 = stmt1.executeQuery("SELECT * FROM accounts WHERE id = 1 FOR UPDATE");
// Держит эксклюзивную блокировку на строку

// Транзакция 2 (пытается получить доступ к той же строке)
conn2.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
conn2.setAutoCommit(false);
Statement stmt2 = conn2.createStatement();
ResultSet rs2 = stmt2.executeQuery("SELECT * FROM accounts WHERE id = 1 FOR UPDATE");
// БЛОКИРУЕТСЯ, ждёт, пока транзакция 1 завершится

conn1.commit(); // Транзакция 1 коммитится, освобождает блокировку
// Теперь транзакция 2 может продолжить
conn2.commit();

2. MVCC (Multi-Version Concurrency Control)

В некоторых СУБД (PostgreSQL, Oracle) используется MVCC с Serializable изоляцией. Каждой транзакции видна снимок данных на момент её начала:

-- Транзакция 1 начинается в момент времени T1
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE id = 1; -- Видит снимок T1
-- ... бизнес-логика ...
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT;

-- Транзакция 2 начинается в момент T2 (параллельно)
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE id = 1; -- Видит снимок T2
-- Если обе транзакции пытаются изменить одну строку, одна будет откачена
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT; -- Может выдать ошибку SERIALIZATION CONFLICT

Lost Update Problem (Проблема потерянного обновления)

Serializable уровень решает эту классическую проблему:

// БЕЗ Serializable (READ COMMITTED)
// Счёт изначально: 1000

// Транзакция 1 (Клиент 1)
conn1.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
ResultSet rs1 = stmt1.executeQuery("SELECT balance FROM accounts WHERE id = 1");
// Прочитал: 1000
Thread.sleep(2000); // Имитация долгой работы
stmt1.executeUpdate("UPDATE accounts SET balance = 1100 WHERE id = 1"); // +100
conn1.commit();

// Транзакция 2 (Клиент 2)
conn2.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
ResultSet rs2 = stmt2.executeQuery("SELECT balance FROM accounts WHERE id = 1");
// Прочитал: 1000 (до коммита транзакции 1)
Thread.sleep(1000);
stmt2.executeUpdate("UPDATE accounts SET balance = 1050 WHERE id = 1"); // +50
conn2.commit();

// РЕЗУЛЬТАТ: balance = 1050 (потеряно обновление +100 из транзакции 1)

// С Serializable
// Транзакция 1 получает блокировку
// Транзакция 2 ждёт, пока транзакция 1 завершится
// Транзакция 2 видит уже обновлённое значение 1100, добавляет +50
// РЕЗУЛЬТАТ: balance = 1150 (всё правильно)

Практические примеры в Java

Spring Data JPA

@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);
}

@Service
public class TransferService {
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepo.findByIdWithLock(fromId)
            .orElseThrow(() -> new AccountNotFoundException());
        Account to = accountRepo.findByIdWithLock(toId)
            .orElseThrow(() -> new AccountNotFoundException());
        
        from.withdraw(amount);
        to.deposit(amount);
        
        // Автоматический commit при успехе
    }
}

Hibernate

@Entity
@Table(name = "accounts")
public class Account {
    @Id
    private Long id;
    private BigDecimal balance;
    // ...
}

@Service
public class AccountService {
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void updateBalance(Long accountId, BigDecimal newBalance) {
        Account account = entityManager.find(Account.class, accountId, LockModeType.PESSIMISTIC_WRITE);
        account.setBalance(newBalance);
        entityManager.flush(); // Явный flush перед потенциальным конфликтом
    }
}

Выбор уровня изоляции

УровеньСкоростьБезопасностьКогда использовать
READ UNCOMMITTEDВысокаяНизкаяРедко (статистика, кэши)
READ COMMITTEDХорошаяХорошаяПо умолчанию, большинство случаев
REPEATABLE READСредняяВысокаяКогда нужна стабильность данных в транзакции
SERIALIZABLEНизкаяМаксимальнаяКритичные финансовые операции

Выводы

  1. Serializable не выбирает — все транзакции попадают в БД, но выполняются так, как если бы это было последовательно
  2. Механизм — блокировки или MVCC с детектором конфликтов
  3. Цена — высокая производительность из-за блокирования
  4. Когда использовать — только для критичных финансовых операций
  5. На практике используй READ COMMITTED с явными блокировками (FOR UPDATE) вместо Serializable для лучшей производительности
Как Serializable выбирает, какая транзакция попадет в базу данных | PrepBro