← Назад к вопросам
Зачем нужен 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: блокирует account 1
- Сеанс 2: блокирует account 2
- Сеанс 1: ждёт account 2 (заблокирован Сеансом 2)
- Сеанс 2: ждёт account 1 (заблокирован Сеансом 1)
- Циклическая зависимость → 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, который:
- Отслеживает граф ожидания (wait-for graph)
- Обнаруживает циклы в этом графе
- Выбирает жертву (victim) — транзакцию с наименьшими затратами
- Откатывает жертву (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'ов:
- Упорядочиваемость: всегда лочи ресурсы в одном порядке
- Минимизация: держи блокировки как можно меньше
- Retry: обрабатывай deadlock'и с exponential backoff
- Мониторинг: логируй все deadlock'и для анализа
Deadlock'и — это неизбежный побочный эффект параллелизма в БД. Профессиональный разработчик должен уметь их предотвращать и обрабатывать.