Как бороться с deadlock-ами в базе данных
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Как бороться с 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;
}
}
Лучшие практики
- Всегда используй один порядок блокировок (Math.min/max)
- Минимизируй время транзакции - долгие операции вне транзакции
- Используй оптимистичные блокировки когда возможно
- Устанавливай timeout для транзакций
- Используй SKIP LOCKED для очередей
- Логируй и мониторь deadlock-и
- Делай retry-и при deadlock-е
- Профилируй запросы
Диагностика
MySQL: SHOW PROCESSLIST; SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; PostgreSQL: SELECT * FROM pg_stat_activity; Oracle: SELECT * FROM v$lock;
Deadlock-и - признак неправильной архитектуры параллелизма. Решение всегда в минимизации времени держания lock-ов.