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

Какие знаешь виды блокировок при работе с базой данных?

1.0 Junior🔥 161 комментариев
#Базы данных и SQL

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

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

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

Виды блокировок при работе с базой данных

Блокировки (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

АспектOptimisticPessimistic
ПроизводительностьБыстрая (нет блокировок при чтении)Медленнее (блокировки)
Когда лучшеМало конфликтовМного конфликтов
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() { }  // Полная изоляция (медленно)

Резюме

Виды блокировок:

  1. Shared Lock — много читателей, нет писателей
  2. Exclusive Lock — один писатель, никто больше
  3. Optimistic Locking — версионирование, проверка при сохранении
  4. Pessimistic Locking — блокировка при чтении
  5. Row-level — на уровне строки (лучшая гранулярность)
  6. Page-level — на уровне страницы (компромисс)
  7. Table-level — на уровне таблицы (самый плохой)

Лучшая практика:

  • Используй Optimistic для большинства случаев
  • Pessimistic только при высоком конфликте
  • Упорядочивай доступ к ресурсам чтобы избежать deadlock'ов
  • Минимизируй время удержания блокировок
  • Используй SELECT FOR UPDATE SKIP LOCKED для распределённой обработки