Какие знаешь виды блокировок при работе с базой данных?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Виды блокировок при работе с базой данных
Блокировки (locks) — фундаментальный механизм управления одновременным доступом к данным БД. Понимание разных видов критично для оптимизации производительности.
1. Shared Lock (Читающая блокировка)
Shared Lock позволяет нескольким транзакциям одновременно читать один ресурс.
-- Transaction 1: читает с блокировкой
SELECT balance FROM accounts WHERE id = 1 WITH (NOLOCK); -- SQL Server
SELECT balance FROM accounts WHERE id = 1 LOCK IN SHARE MODE; -- MySQL
-- Transaction 2: может ОДНОВРЕМЕННО читать
SELECT balance FROM accounts WHERE id = 1; -- Успешно
-- Transaction 2: НЕ может писать
UPDATE accounts SET balance = 1000 WHERE id = 1; -- Ждёт, пока T1 отпустит блокировку
Использование в Java/Hibernate:
@Transactional(readOnly = true)
public BigDecimal getAccountBalance(Long accountId) {
// По умолчанию: Read Committed isolation + Shared Lock
return accountRepository.findById(accountId)
.map(Account::getBalance)
.orElse(BigDecimal.ZERO);
}
2. Exclusive Lock (Исключительная блокировка)
Exclusive Lock блокирует ресурс для всех остальных транзакций.
-- Transaction 1: пишет с блокировкой
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- id=1 заблокирован ЭКСКЛЮЗИВНО
-- Transaction 2: пытается читать
SELECT balance FROM accounts WHERE id = 1; -- ЖДЁТ!
-- Transaction 2: пытается писать
UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- ЖДЁТ!
-- Transaction 1: освобождает блокировку
COMMIT; -- Теперь T2 может продолжить
Использование в Java:
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account sender = accountRepository.findById(fromId).orElseThrow();
Account recipient = accountRepository.findById(toId).orElseThrow();
// Обе записи получают Exclusive Locks автоматически
sender.setBalance(sender.getBalance().subtract(amount));
recipient.setBalance(recipient.getBalance().add(amount));
accountRepository.saveAll(List.of(sender, recipient));
} // COMMIT — блокировки отпускаются
3. Optimistic Locking (Оптимистичная блокировка)
Не блокирует при чтении. Проверяет конфликты при записи.
@Entity
public class Account {
@Id
private Long id;
private BigDecimal balance;
@Version // Hibernate управляет версией автоматически
private Long version; // Увеличивается при каждом обновлении
}
// Использование
@Transactional
public void updateAccount(Long id, BigDecimal newBalance) {
Account account = accountRepository.findById(id).orElseThrow();
// version = 1 (загружена с записью)
// Другой поток может изменить account между findById и save
// Но мы об этом узнаем!
account.setBalance(newBalance);
accountRepository.save(account);
// UPDATE accounts SET balance = ?, version = 2 WHERE id = ? AND version = 1
// Если version не 1, будет OptimisticLockException
}
Сценарий конфликта:
public void demonstrateOptimisticLockConflict() throws Exception {
Account account = accountRepository.findById(1L).orElseThrow();
// version = 1
// Другой поток изменяет запись
accountRepository.save(Account.withId(1L).setVersion(2));
// version в БД теперь = 2
// Мы пытаемся сохранить с version = 1
account.setBalance(BigDecimal.valueOf(500));
try {
accountRepository.save(account);
// UPDATE accounts SET balance = ?, version = 2 WHERE id = 1 AND version = 1
// 0 rows affected! — OptimisticLockException
} catch (OptimisticLockException e) {
// Нужно перезагрузить и повторить операцию
}
}
4. Pessimistic Locking (Пессимистичная блокировка)
Явно блокирует запись при чтении.
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
// PESSIMISTIC_READ: Shared Lock
@Query("SELECT a FROM Account a WHERE a.id = :id")
@Lock(LockModeType.PESSIMISTIC_READ)
Optional<Account> findByIdWithReadLock(@Param("id") Long id);
// PESSIMISTIC_WRITE: Exclusive Lock
@Query("SELECT a FROM Account a WHERE a.id = :id")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Account> findByIdWithWriteLock(@Param("id") Long id);
// PESSIMISTIC_FORCE_INCREMENT: Exclusive Lock + version increment
@Query("SELECT a FROM Account a WHERE a.id = :id")
@Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT)
Optional<Account> findByIdWithForceIncrement(@Param("id") Long id);
}
// Использование
@Transactional
public void criticalTransfer(Long fromId, Long toId, BigDecimal amount) {
// PESSIMISTIC_WRITE блокирует запись ДО чтения
Account sender = accountRepository.findByIdWithWriteLock(fromId)
.orElseThrow();
Account recipient = accountRepository.findByIdWithWriteLock(toId)
.orElseThrow();
// Никой другой транзакции не может читать или писать эти записи
sender.setBalance(sender.getBalance().subtract(amount));
recipient.setBalance(recipient.getBalance().add(amount));
accountRepository.saveAll(List.of(sender, recipient));
}
5. Row-level Lock (Блокировка на уровне строки)
БД блокирует отдельные строки, а не целые таблицы.
-- T1: блокирует только row с id=1
BEGIN;
UPDATE accounts SET balance = 500 WHERE id = 1;
-- Только эта строка заблокирована
-- T2: может одновременно обновлять row с id=2
UPDATE accounts SET balance = 600 WHERE id = 2; -- Успешно!
-- T2: но не может обновлять id=1
UPDATE accounts SET balance = 700 WHERE id = 1; -- ЖДЁТ
6. Page-level Lock (Блокировка на уровне страницы)
БД блокирует целую страницу памяти (обычно 8KB).
-- SQL Server может использовать page locks для больших операций
ALTER INDEX idx_accounts ON accounts REBUILD WITH (ALLOW_ROW_LOCKS = OFF);
-- Теперь используются только page locks, не row locks
7. Table-level Lock (Блокировка на уровне таблицы)
БД блокирует целую таблицу.
-- MySQL
LOCK TABLES accounts WRITE; -- Эксклюзивная блокировка таблицы
UPDATE accounts SET balance = 0;
UNLOCK TABLES;
-- Другие транзакции: все ЖДУТ
SELECT * FROM accounts; -- ЖДЁТ
UPDATE accounts SET balance = 100 WHERE id = 1; -- ЖДЁТ
8. Deadlock (Взаимная блокировка)
Две транзакции ждут друг друга.
-- Transaction 1
BEGIN;
UPDATE accounts SET balance = 500 WHERE id = 1; -- Блокирует id=1
WAIT... -- Пытается получить доступ к id=2
-- Transaction 2 (параллельно)
BEGIN;
UPDATE accounts SET balance = 600 WHERE id = 2; -- Блокирует id=2
WAIT... -- Пытается получить доступ к id=1
-- DEADLOCK: обе транзакции ждут друг друга
-- БД обнаруживает это и откатывает одну из них
-- ERROR: Deadlock detected
Решение:
@Transactional(isolation = Isolation.SERIALIZABLE,
timeout = 5)
public void transferWithRetry(Long fromId, Long toId, BigDecimal amount) {
// Упорядочиваем доступ (всегда сначала меньший id)
Long first = Math.min(fromId, toId);
Long second = Math.max(fromId, toId);
Account acc1 = accountRepository.findByIdWithWriteLock(first).orElseThrow();
Account acc2 = accountRepository.findByIdWithWriteLock(second).orElseThrow();
// Deadlock маловероятен благодаря упорядочиванию
}
9. Comparison: Optimistic vs Pessimistic
| Аспект | Optimistic | Pessimistic |
|---|---|---|
| Производительность | Быстрая (нет блокировок при чтении) | Медленнее (блокировки) |
| Когда лучше | Мало конфликтов | Много конфликтов |
| Deadlock'и | Невозможны | Возможны |
| Масштабируемость | Лучше для высокой параллелизации | Хуже при много транзакций |
| Retry логика | Нужна обработка OptimisticLockException | Потоки просто ждут |
| Когда использовать | Web приложения, REST API | БД критичная для консистентности |
10. SELECT FOR UPDATE
Явное получение Pessimistic Lock на выбранных строках:
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o WHERE o.id = :id FOR UPDATE")
Optional<Order> findByIdForUpdate(@Param("id") Long id);
@Query("SELECT o FROM Order o WHERE o.status = 'PENDING' FOR UPDATE SKIP LOCKED")
List<Order> findPendingOrdersWithLock();
}
// SELECT FOR UPDATE SKIP LOCKED: пропускает уже заблокированные записи
public void processPendingOrders() {
List<Order> orders = orderRepository.findPendingOrdersWithLock();
// Заблокировали только заказы, которые никто не обрабатывает
for (Order order : orders) {
processOrder(order);
}
}
11. Levels of Isolation
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void level1() { } // Dirty reads возможны
@Transactional(isolation = Isolation.READ_COMMITTED)
public void level2() { } // Dirty reads защищены (default)
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void level3() { } // Phantom reads защищены
@Transactional(isolation = Isolation.SERIALIZABLE)
public void level4() { } // Полная изоляция (медленно)
Резюме
Виды блокировок:
- Shared Lock — много читателей, нет писателей
- Exclusive Lock — один писатель, никто больше
- Optimistic Locking — версионирование, проверка при сохранении
- Pessimistic Locking — блокировка при чтении
- Row-level — на уровне строки (лучшая гранулярность)
- Page-level — на уровне страницы (компромисс)
- Table-level — на уровне таблицы (самый плохой)
Лучшая практика:
- Используй Optimistic для большинства случаев
- Pessimistic только при высоком конфликте
- Упорядочивай доступ к ресурсам чтобы избежать deadlock'ов
- Минимизируй время удержания блокировок
- Используй SELECT FOR UPDATE SKIP LOCKED для распределённой обработки