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

Зачем нужен deadlock?

2.0 Middle🔥 131 комментариев
#Асинхронность и многопоточность#Базы данных (SQL)

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

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

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

Deadlock в базах данных и многопоточности

Очень важный вопрос! Нужно уточнить: вопрос идёт про deadlock в контексте базы данных или про управление deadlock'ом в целом. Дам развёрнутый ответ на оба.

Часть 1: Что такое deadlock

Deadlock — это ситуация, когда две (или более) транзакции ждут друг друга, создавая циклическую зависимость. Ни одна из них не может продолжать работу.

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

Транзакция A         →    Транзакция B
├─ Блокирует Table X
│
└─ Ждёт Table Y    ←──────  Блокирует Table Y
                            Ждёт Table X

Оба ждут друг друга. DEADLOCK!

Визуальный пример в PostgreSQL

# Сеанс 1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- Заблокирован account 1
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- Ждёт разблокирования account 2

# Сеанс 2 (в параллельном окне)
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- Заблокирован account 2
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- Ждёт разблокирования account 1
# DEADLOCK DETECTED!

Повторим шаги:

  1. Сеанс 1: блокирует account 1
  2. Сеанс 2: блокирует account 2
  3. Сеанс 1: ждёт account 2 (заблокирован Сеансом 2)
  4. Сеанс 2: ждёт account 1 (заблокирован Сеансом 1)
  5. Циклическая зависимость → DEADLOCK

Почему deadlock'и случаются и как их избежать

1. Причина: неупорядоченные блокировки

ПЛОХО:

# transaction_a.py
db.lock(resource_1)
time.sleep(0.1)  # Даёт время другой транзакции заблокировать resource_2
db.lock(resource_2)  # Зависим от resource_2

# transaction_b.py
db.lock(resource_2)
time.sleep(0.1)
db.lock(resource_1)  # Зависим от resource_1
# DEADLOCK!

ХОРОШО:

# Всегда заблокируй ресурсы в одном порядке!
# transaction_a.py
db.lock(resource_1)  # Сначала 1
db.lock(resource_2)  # Потом 2

# transaction_b.py
db.lock(resource_1)  # Сначала 1
db.lock(resource_2)  # Потом 2
# Deadlock невозможен!

2. Практический пример: транзакция денежного перевода

class MoneyTransfer:
    """Правильная реализация без deadlock'а"""
    
    def transfer(self, from_account_id, to_account_id, amount):
        """
        ВАЖНО: всегда лочим в порядке по ID (от меньшего к большему)
        Это гарантирует, что нет циклических зависимостей
        """
        
        # Упорядочиваем ID — гарантирует консистентный порядок
        first_id = min(from_account_id, to_account_id)
        second_id = max(from_account_id, to_account_id)
        
        with transaction.atomic():
            # Блокируем в консистентном порядке
            acc1 = Account.objects.select_for_update().get(id=first_id)
            acc2 = Account.objects.select_for_update().get(id=second_id)
            
            # Теперь определяем, кто отправитель, кто получатель
            if acc1.id == from_account_id:
                sender = acc1
                receiver = acc2
            else:
                sender = acc2
                receiver = acc1
            
            # Проверяем баланс
            if sender.balance < amount:
                raise InsufficientFundsError()
            
            # Переводим деньги
            sender.balance -= amount
            receiver.balance += amount
            
            sender.save()
            receiver.save()

Часть 2: Как база данных ОБНАРУЖИВАЕТ и РЕШАЕТ deadlock'и

Обнаружение deadlock'а

Базы данных (PostgreSQL, MySQL) имеют встроенный deadlock detector, который:

  1. Отслеживает граф ожидания (wait-for graph)
  2. Обнаруживает циклы в этом графе
  3. Выбирает жертву (victim) — транзакцию с наименьшими затратами
  4. Откатывает жертву (ROLLBACK)
# PostgreSQL error при deadlock:
# psycopg2.extensions.TransactionRollbackError: 
# deadlock detected

Обработка deadlock'а в коде

import time
from django.db import transaction
from psycopg2 import OperationalError

def transfer_with_retry(from_id, to_id, amount, max_retries=3):
    """Транзакция с повтором при deadlock'е"""
    
    for attempt in range(max_retries):
        try:
            with transaction.atomic():
                # Упорядочиваем ID
                first_id = min(from_id, to_id)
                second_id = max(from_id, to_id)
                
                # Лочим в консистентном порядке
                acc1 = Account.objects.select_for_update().get(id=first_id)
                acc2 = Account.objects.select_for_update().get(id=second_id)
                
                # Выполняем транзакцию
                if acc1.id == from_id:
                    acc1.balance -= amount
                    acc2.balance += amount
                else:
                    acc2.balance -= amount
                    acc1.balance += amount
                
                acc1.save()
                acc2.save()
                
                return True  # Успешно
        
        except OperationalError as e:
            if 'deadlock' in str(e).lower():
                if attempt < max_retries - 1:
                    wait_time = 0.1 * (2 ** attempt)  # Exponential backoff
                    logger.warning(f"Deadlock detected, retrying in {wait_time}s")
                    time.sleep(wait_time)
                    continue
                else:
                    logger.error(f"Deadlock after {max_retries} attempts")
                    raise
            else:
                raise
    
    return False

Практические примеры deadlock'ов в реальной жизни

Пример 1: Foreign Keys и каскадное удаление

class Department(models.Model):
    name = models.CharField(max_length=100)

class Employee(models.Model):
    name = models.CharField(max_length=100)
    department = models.ForeignKey(
        Department, 
        on_delete=models.CASCADE  # Каскадное удаление
    )

# Deadlock:
# Тред 1: DELETE FROM department WHERE id = 1
#         → Лочит department
#         → Пытается удалить всех employees (ждёт employee table)
# 
# Тред 2: UPDATE employee SET department_id = 2 WHERE id = 100
#         → Лочит employee
#         → Проверяет FK constraint на department (ждёт department table)

Пример 2: Индексы и блокировки

# ПЛОХО: обновляем в разных порядках
# Тред 1:
UPDATE users SET status = 'active' WHERE id = 10;
UPDATE users SET status = 'active' WHERE id = 20;

# Тред 2:
UPDATE users SET status = 'active' WHERE id = 20;
UPDATE users SET status = 'active' WHERE id = 10;
# Deadlock!

# ХОРОШО: всегда в одном порядке
UPDATE users SET status = 'active' WHERE id IN (10, 20) ORDER BY id;

Стратегии предотвращения deadlock'ов

1. Порядок блокировок (Ordering)

# ПРАВИЛО: если обновляешь несколько ресурсов, лочи в одном порядке
resources = sorted([resource_a, resource_b, resource_c])
for res in resources:
    res.acquire_lock()

2. Timeouts и откаты

# PostgreSQL
BEGIN;
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '10s';
-- Твои операции
COMMIT;

3. Изоляция транзакций

# SERIALIZABLE изоляция (строже, но медленнее)
with transaction.atomic():
    with connection.cursor() as cursor:
        cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;")
        # операции

4. Минимизация времени блокировки

# ПЛОХО: долгая транзакция
with transaction.atomic():
    user = User.objects.select_for_update().get(id=1)
    result = expensive_calculation()  # 5 секунд!
    user.data = result
    user.save()

# ХОРОШО: расчёты ВНЕ транзакции
result = expensive_calculation()  # 5 секунд ДО транзакции
with transaction.atomic():
    user = User.objects.select_for_update().get(id=1)
    user.data = result
    user.save()

Мониторинг deadlock'ов

-- PostgreSQL: смотри логи deadlock'ов
SELECT * FROM pg_stat_database 
WHERE deadlocks > 0;

-- MySQL: смотри LATEST DETECTED DEADLOCK
SHOW ENGINE INNODB STATUS\G

Вывод

Deadlock'и нужны понимать потому что:

  • ✅ Они происходят в распределённых системах с параллельными транзакциями
  • ✅ Влияют на reliability приложения
  • ✅ Требуют правильной обработки (retry, logging)
  • ✅ Легко заработать deadlock, если не думать про порядок блокировок

Как избежать deadlock'ов:

  1. Упорядочиваемость: всегда лочи ресурсы в одном порядке
  2. Минимизация: держи блокировки как можно меньше
  3. Retry: обрабатывай deadlock'и с exponential backoff
  4. Мониторинг: логируй все deadlock'и для анализа

Deadlock'и — это неизбежный побочный эффект параллелизма в БД. Профессиональный разработчик должен уметь их предотвращать и обрабатывать.

Зачем нужен deadlock? | PrepBro