Как Serializable выбирает, какая транзакция попадет в базу данных
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Serializable уровень изоляции транзакций
Этот вопрос касается уровня изоляции транзакций Serializable в контексте ACID-гарантий и механики работы СУБД. Важно понимать, что Serializable не выбирает, какая транзакция попадёт в БД — все должны попасть. Вместо этого он гарантирует, что транзакции выполняются так, как если бы они выполнялись последовательно (serialization), несмотря на параллельное выполнение.
Что такое уровни изоляции транзакций
В SQL (и Java JDBC) есть 4 стандартных уровня изоляции транзакций:
- READ UNCOMMITTED — самый низкий, может читать незафиксированные (dirty reads) изменения других транзакций
- READ COMMITTED — не читаем незафиксированные изменения (стандарт для большинства БД)
- REPEATABLE READ — дополнительно гарантирует стабильность прочитанных данных внутри одной транзакции
- 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 | Низкая | Максимальная | Критичные финансовые операции |
Выводы
- Serializable не выбирает — все транзакции попадают в БД, но выполняются так, как если бы это было последовательно
- Механизм — блокировки или MVCC с детектором конфликтов
- Цена — высокая производительность из-за блокирования
- Когда использовать — только для критичных финансовых операций
- На практике используй READ COMMITTED с явными блокировками (FOR UPDATE) вместо Serializable для лучшей производительности