Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Как достигается очередь (Serialization) при SERIALIZABLE уровне изоляции
Что означает SERIALIZABLE
SERIALIZABLE - это самый строгий уровень изоляции транзакций. Он гарантирует, что транзакции выполняются как будто они идут одна за другой (serially), а не параллельно.
При SERIALIZABLE:
Трансакция 1 ┌───────────────┐
│ (выполняется) │
└───────────────┘
↓
Транзакция 2 ┌───────────────┐
│ (выполняется) │
└───────────────┘
↓
Транзакция 3 ┌───────────────┐
│ (выполняется) │
└───────────────┘
Все транзакции как в очереди (serialization)
Проблема без SERIALIZABLE
Phantom Read
Трансакция 1:
SELECT COUNT(*) FROM accounts;
Результат: 100 аккаунтов
Трансакция 2 (параллельно):
INSERT INTO accounts VALUES (...);
COMMIT;
Трансакция 1:
SELECT COUNT(*) FROM accounts;
Результат: 101 аккаунт ← Phantom!
Это проблема Phantom Read - данные изменились между SELECT запросами в одной транзакции.
Как достигается очередь
1. Pessimistic Locking (Пессимистическое блокирование)
Идея: Блокировать все возможные конфликты ДО того, как они произойдут.
// Транзакция 1
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// SELECT ... FOR UPDATE (экстензивная блокировка)
Account from = accountRepository.findByIdForUpdateSerializable(fromId);
Account to = accountRepository.findByIdForUpdateSerializable(toId);
// БД блокирует ОБЕ строки и весь диапазон
// Другие транзакции ждут
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
// COMMIT - блокировки освобождаются
// Другие транзакции могут выполняться
}
Трансакция 1 начало:
┌──────────────────────────────┐
│ LOCK row 1 (EXCLUSIVE) │ ← блокировка
│ LOCK row 2 (EXCLUSIVE) │ ← блокировка
└──────────────────────────────┘
Трансакция 2 (параллельно):
┌──────────────────────────────┐
│ Ждёт... LOCK row 1 (WAITING) │ ← ждёт освобождения
│ Ждёт... LOCK row 2 (WAITING) │ ← ждёт освобождения
└──────────────────────────────┘
Трансакция 1 конец:
┌──────────────────────────────┐
│ UNLOCK all │ ← освобождаю блокировки
│ COMMIT │
└──────────────────────────────┘
↓
Трансакция 2 (продолжает):
┌──────────────────────────────┐
│ LOCK row 1 (ACQUIRED) │ ← получила блокировку
│ LOCK row 2 (ACQUIRED) │ ← получила блокировку
│ (выполнять) │
└──────────────────────────────┘
2. Predicate Locking (Блокирование предиката)
Для защиты от Phantom Read, БД блокирует не только строки, но и диапазоны.
// Транзакция 1
@Transactional(isolation = Isolation.SERIALIZABLE)
public void calculateAverageBalance() {
// SELECT ... WHERE balance > 1000
List<Account> richAccounts =
accountRepository.findAccountsWithBalanceGreaterThan(1000);
// БД блокирует НЕ ТОЛЬКО найденные строки
// НО И весь диапазон WHERE balance > 1000
// Это предотвращает INSERT новых строк в этот диапазон
double avgBalance = richAccounts.stream()
.mapToDouble(Account::getBalance)
.average()
.orElse(0);
}
// Транзакция 2 (параллельно)
public void insertNewRichAccount() {
Account newAccount = new Account();
newAccount.setBalance(BigDecimal.valueOf(5000));
// БЛОКИРОВАНА! Потому что:
// 1. Диапазон (balance > 1000) заблокирован транзакцией 1
// 2. New account имеет balance 5000 > 1000
// 3. Нужно ждать освобождения диапазона
accountRepository.save(newAccount); // WAITING
}
Трансакция 1:
┌────────────────────────────────────────┐
│ SELECT * WHERE balance > 1000 │
│ LOCK range [1000, ∞) EXCLUSIVE │ ← Блокирую весь диапазон!
│ (читаю данные) │
└────────────────────────────────────────┘
Трансакция 2 (параллельно):
┌────────────────────────────────────────┐
│ INSERT (balance=5000) │
│ Пытается вставить в range [1000, ∞) │
│ WAITING на освобождение range lock │ ← Блокирована!
└────────────────────────────────────────┘
Механизм очереди: версионирование (в PostgreSQL)
MVCC (Multi-Version Concurrency Control)
Постгрес не просто блокирует - он хранит несколько версий данных!
Тип данных Account (версионирование):
xmin=100 (версия начинается с транзакции 100)
xmax=200 (версия заканчивается в транзакции 200)
Трансакция 100:
INSERT Account(balance=1000) xmin=100
Трансакция 150:
SELECT... ← видит версию где xmin=100, xmax=NULL
Трансакция 200:
UPDATE Account SET balance=2000
← старая версия: xmin=100, xmax=200
← новая версия: xmin=200, xmax=NULL
Трансакция 150:
SELECT... ← ВСЕГДА видит ту же версию (снимок на 150)
Трансакция 300:
SELECT... ← видит новую версию (xmin=200)
Пример: Очередь в действии
// Сценарий: двое пытаются вывести деньги с одного счёта
Аккаунт Account(id=1, balance=1000)
Трансакция 1 (Пользователь А):
@Transactional(isolation = Isolation.SERIALIZABLE)
public void withdraw() {
Account account = repo.findByIdForUpdate(1); // LOCK ACQUIRED
// balance = 1000
if (account.getBalance() >= 500) {
account.setBalance(500);
repo.save(account);
Thread.sleep(5000); // Имитирую долгую операцию
}
// COMMIT - блокировка освобождена
}
Трансакция 2 (Пользователь В, через 1 сек):
@Transactional(isolation = Isolation.SERIALIZABLE)
public void withdraw() {
Account account = repo.findByIdForUpdate(1); // WAITING на lock!
// Ждёт 4 сек пока транзакция 1 не завершится
// Затем получает блокировку
// account.balance = 500 (изменение из транзакции 1)
if (account.getBalance() >= 500) {
account.setBalance(0);
repo.save(account);
}
// COMMIT
}
// Результат:
// Оба вывода выполнены последовательно (как очередь)
// Не было двойного вывода денег
// SERIALIZABLE гарантирует консистентность
Уровни блокирования
┌──────────────────────┬──────────────────────┐
│ Тип Блокировки │ Что блокируется │
├──────────────────────┼──────────────────────┤
│ Record Lock │ Отдельная строка │
│ Range Lock │ Диапазон строк │
│ Table Lock │ Вся таблица │
│ Gap Lock │ "Промежуток" между │
│ │ строками (phantom) │
└──────────────────────┴──────────────────────┘
Конфигурация в Spring
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
// READ COMMITTED (по умолчанию)
@Query("SELECT a FROM Account a WHERE a.id = ?1")
Account findById(Long id);
// REPEATABLE READ
@Query("SELECT a FROM Account a WHERE a.id = ?1 FOR UPDATE")
Account findByIdForUpdate(Long id);
// SERIALIZABLE
@Query(value =
"SELECT * FROM accounts WHERE id = ?1 FOR UPDATE",
nativeQuery = true
)
Account findByIdForUpdateSerializable(Long id);
}
@Service
public class TransferService {
@Autowired
private AccountRepository accountRepository;
// Уровень транзакции: SERIALIZABLE
@Transactional(
isolation = Isolation.SERIALIZABLE,
propagation = Propagation.REQUIRED,
rollbackFor = Exception.class
)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
// Блокирую оба счёта
Account from = accountRepository.findByIdForUpdateSerializable(fromId);
Account to = accountRepository.findByIdForUpdateSerializable(toId);
// Во время этого блока другие транзакции ЖДУТ
// → эффект очереди
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
// COMMIT - блокировки освобождаются
// Ждущие транзакции могут продолжить
}
}
Цена очереди: Deadlocks
Осторожно с порядком блокировок!
// ❌ DEADLOCK ВОЗМОЖЕН!
Трансакция 1:
LOCK account 1
WAIT account 2 ← ждёт на 2
Трансакция 2:
LOCK account 2
WAIT account 1 ← ждёт на 1
→ DEADLOCK!
// ✓ ПРАВИЛЬНО - одинаковый порядок
Трансакция 1:
LOCK account 1 (min id)
LOCK account 2 (max id)
Трансакция 2:
LOCK account 1 (min id) ← ждёт на 1
LOCK account 2 (max id)
// Нет deadlock, потому что порядок одинаковый
Производительность SERIALIZABLE
┌─────────────────┬──────────────┬──────────────┐
│ Уровень │ Скорость │ Безопасность │
├─────────────────┼──────────────┼──────────────┤
│ READ UNCOMMITTED│ Очень быстро │ Низкая │
│ READ COMMITTED │ Быстро │ Средняя │
│ REPEATABLE READ │ Медленнее │ Высокая │
│ SERIALIZABLE │ Очень медленно│ Максимальная │
└─────────────────┴──────────────┴──────────────┘
SERIALIZABLE медленен, потому что:
- Много блокировок
- Другие транзакции ждут
- Нет параллелизма
Когда использовать SERIALIZABLE
✓ Используй для критичных операций
- Финансовые транзакции
- Резервирование мест (авиа, кино)
- Инвентаризация
- Любое где нельзя ошибиться
❌ НЕ используй для
- Просмотра статей/новостей
- Поиска по каталогу
- Чтения некритичных данных
- High-frequency операций
Заключение
Очередь при SERIALIZABLE достигается через:
- Pessimistic Locking - блокируем ВСЕ конфликты заранее
- Predicate Locking - блокируем диапазоны (для phantom reads)
- MVCC (в некоторых БД) - версионирование данных
- Sequence of locks - гарантируем эффект очереди
Результат: Транзакции выполняются как одна за другой, гарантируя максимальную консистентность.