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

Как бороться с deadlock-ами в базе данных

3.0 Senior🔥 111 комментариев
#Базы данных и SQL

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

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

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

# Как бороться с deadlock-ами в базе данных

Что такое deadlock

Deadlock - это ситуация, когда две или более транзакции ждут друг друга, создавая циклическое ожидание. Ни одна из них не может продвинуться, и они зависают навсегда, пока БД не прервёт одну из транзакций.

Классический пример

Транзакция 1 блокирует Table A и ждёт Table B. Транзакция 2 блокирует Table B и ждёт Table A. Обе зависли - DEADLOCK!

Причины deadlock-ов

1. Различный порядок блокировок

Транзакция 1: lock User 1 → lock User 2 Транзакция 2: lock User 2 → lock User 1 Это может привести к deadlock-у.

2. Долгие транзакции

Если транзакция держит lock долго (30 сек), возрастает вероятность deadlock-а.

3. Вложенные блокировки в разном порядке

Методы борьбы с deadlock-ами

1. Всегда используй один порядок блокировок

public void transfer(Long id1, Long id2, BigDecimal amount) {
    // Всегда сначала меньший ID, потом больший
    Long first = Math.min(id1, id2);
    Long second = Math.max(id1, id2);
    
    User user1 = lock(first);
    User user2 = lock(second);
}

2. Используй SELECT FOR UPDATE с правильным порядком

@Transactional
public void updateTwoAccounts(Long account1, Long account2) {
    List<Long> ids = Arrays.asList(account1, account2);
    Collections.sort(ids);
    
    // Всегда получим в одном порядке
    List<Account> accounts = em.createQuery(
        "SELECT a FROM Account a WHERE a.id IN :ids ORDER BY a.id FOR UPDATE",
        Account.class
    ).setParameter("ids", ids).getResultList();
}

3. Минимизируй время транзакции

// Плохо - долгие операции в транзакции
@Transactional
public void processOrder(Long orderId) {
    Order order = orderRepository.findById(orderId).get();
    order.setStatus("PROCESSING");
    orderRepository.save(order);
    
    callExternalService();  // Может зависнуть
    sendEmail();
}

// Хорошо - только БД операции в транзакции
@Transactional
public Order updateOrderStatus(Long orderId) {
    Order order = orderRepository.findById(orderId).get();
    order.setStatus("PROCESSING");
    return orderRepository.save(order);
}

// Долгие операции вне транзакции
order = updateOrderStatus(orderId);
callExternalService();  // Без lock-а
sendEmail();  // Без lock-а

4. Используй оптимистичные блокировки

@Entity
public class Product {
    @Id
    private Long id;
    
    @Version
    private Long version;  // Автоматическая версия
    
    private String name;
}

// Использование
@Transactional
public void updateProduct(Long id, String newName) {
    Product product = productRepository.findById(id).get();
    product.setName(newName);
    // Если другая транзакция изменила product,
    // будет выброшено OptimisticLockingFailureException
    productRepository.save(product);
}

5. Используй низкие уровни изоляции

@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateOrder(Long orderId) {
    // READ_COMMITTED уменьшает вероятность deadlock-а
}

6. Используй timeout для операций

@Transactional(timeout = 10)
public void quickOperation() {
    // Если операция дольше 10 сек - будет откачена
}

7. SELECT FOR UPDATE SKIP LOCKED

PostgreSQL и MySQL 8+ поддерживают SKIP LOCKED - пропускает заблокированные строки:

SELECT u FROM User u 
WHERE u.status = 'PENDING' 
FOR UPDATE SKIP LOCKED
LIMIT 10

Полезно для очередей обработки.

8. Используй конкурентные очереди

Вместо синхронной обработки в БД, используй очередь:

ExecutorService executor = Executors.newFixedThreadPool(10);
Queue<Order> orderQueue = new ConcurrentLinkedQueue<>();

// Добавляем в очередь (быстро)
orders.forEach(orderQueue::offer);

// Обрабатываем параллельно
for (int i = 0; i < 10; i++) {
    executor.submit(() -> {
        Order order = orderQueue.poll();
        if (order != null) {
            processOrder(order);
        }
    });
}

9. Автоматический retry

@Service
public class OrderService {
    
    @Retry(
        maxAttempts = 3,
        delay = 1000,
        multiplier = 2.0,
        retryOn = DeadlockLoserDataAccessException.class
    )
    @Transactional
    public void processOrder(Long orderId) {
        // Автоматически повторит до 3 раз при deadlock-е
    }
}

10. Логирование deadlock-ов

@ControllerAdvice
public class DeadlockHandler {
    
    @ExceptionHandler(DataAccessException.class)
    public ResponseEntity<?> handleDeadlock(DataAccessException e) {
        if (e.getCause() instanceof SQLException) {
            SQLException sqlEx = (SQLException) e.getCause();
            // PostgreSQL: 40P01, MySQL: 1213
            if ("40P01".equals(sqlEx.getSQLState())) {
                logger.error("Deadlock detected: " + e.getMessage());
                return ResponseEntity.status(409).body("Deadlock, retry");
            }
        }
        throw e;
    }
}

Лучшие практики

  1. Всегда используй один порядок блокировок (Math.min/max)
  2. Минимизируй время транзакции - долгие операции вне транзакции
  3. Используй оптимистичные блокировки когда возможно
  4. Устанавливай timeout для транзакций
  5. Используй SKIP LOCKED для очередей
  6. Логируй и мониторь deadlock-и
  7. Делай retry-и при deadlock-е
  8. Профилируй запросы

Диагностика

MySQL: SHOW PROCESSLIST; SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; PostgreSQL: SELECT * FROM pg_stat_activity; Oracle: SELECT * FROM v$lock;

Deadlock-и - признак неправильной архитектуры параллелизма. Решение всегда в минимизации времени держания lock-ов.