Инвалидация блокировок транзакций

Инвалидация блокировок транзакций (TLI) происходит, когда одна транзакция (нарушитель) записывает данные и ломает оптимистичные блокировки другой транзакции (жертвы). Жертва обнаруживает это при коммите и получает ошибку transaction locks invalidated. Транзакцию-жертву необходимо повторить. Частые повторы замедляют приложение.

Примечание

YDB SDK предоставляет встроенный механизм повтора временных ошибок. Подробнее см. Обработка ошибок.

Предотвращение конфликтов

  • Сокращайте длительность транзакций. Чем дольше транзакция удерживает блокировки, тем выше вероятность конфликта. По возможности избегайте интерактивных транзакций: лучший подход — один YQL-запрос с BEGIN; и COMMIT; для чтения, изменения и коммита данных. Если интерактивные транзакции необходимы, выполняйте COMMIT в последнем запросе.

  • Сокращайте пересечение данных между транзакциями. Чем меньше строк читает транзакция, тем меньше блокировок она удерживает и тем ниже вероятность конфликта. Избегайте чтения заведомо ненужных данных. Если несколько транзакций конкурируют за одни и те же строки — пересмотрите схему данных.

  • Используйте режимы транзакций только на чтение. Транзакции в режиме Snapshot Read-Only читают данные из консистентного снапшота и не устанавливают оптимистичных блокировок — такая транзакция никогда не станет жертвой TLI. Если транзакция не изменяет данные, явно задавайте этот режим через SDK.

Пример: конфликт транзакций при резервировании товара

В интернет-магазине на распродаже сотни покупателей одновременно покупают один товар. Транзакция покупки читает остаток, потом приложение рассчитывает скидки, ждёт подтверждения оплаты, и только после этого списывает:

SELECT available FROM stock WHERE sku = $sku AND warehouse_id = $warehouse_id;
-- приложение проверяет: available > 0
-- приложение рассчитывает скидки, ожидает оплаты ...
UPDATE stock SET available = available - 1 WHERE sku = $sku AND warehouse_id = $warehouse_id;
COMMIT;

SELECT устанавливает оптимистичную блокировку на строку. Пока транзакция ожидает оплаты, другая транзакция успевает изменить ту же строку и закоммититься — блокировка ломается, и первая транзакция получает TLI при коммите.

Решение — сразу резервировать товар одной короткой транзакцией, а всё медленное (скидки, оплата, внешние сервисы) выполнять после:

BEGIN;
UPDATE stock SET available = available - 1, reserved = reserved + 1
WHERE sku = $sku AND warehouse_id = $warehouse_id AND available > 0;

UPSERT INTO orders (order_id, sku, qty, status)
VALUES ($order_id, $sku, 1, "RESERVED");
COMMIT;
-- после: расчёт скидок, оплата, доставка

Если оплата прошла — отдельной транзакцией ставим status = "PAID". Если нет — возвращаем товар из reserved обратно в available и ставим "CANCELLED". Транзакция резерва выполняется быстро и минимизирует время удержания блокировки.

Диагностика

Мониторинг в Grafana

  1. Откройте панель мониторинга DB overview в Grafana.

  2. Проверьте, есть ли всплески количества ошибок на диаграмме Transaction Lock Invalidation.

    Эта диаграмма отображает количество запросов в секунду, возвращаемых с ошибкой «transaction locks invalidated».

Диагностика через логи TLI

Когда транзакция завершается с ошибкой TLI, сообщение об ошибке содержит идентификатор запроса-жертвы:

Transaction locks invalidated. ... VictimQuerySpanId: 1111111111111111.

По этому VictimQuerySpanId можно найти в логах сервера полный контекст конфликта: какой запрос установил блокировки и какой их сломал. Подробнее о включении логирования, формате записей и корреляции событий, а также об утилите find_tli_chain для автоматического анализа логов см. в Логирование TLI.

Анализ через системные представления

Для анализа конфликтов блокировок доступны следующие системные представления:

Анализ на уровне запросов

Для выявления запросов с наибольшим числом конфликтов используйте системное представление .sys/query_metrics_one_minute:

SELECT QueryText, LocksBrokenAsBreaker, LocksBrokenAsVictim
FROM `.sys/query_metrics_one_minute`
WHERE LocksBrokenAsBreaker > 0 OR LocksBrokenAsVictim > 0
ORDER BY LocksBrokenAsBreaker + LocksBrokenAsVictim DESC;
Колонка Описание
LocksBrokenAsBreaker Сколько раз этот запрос сломал чужие блокировки
LocksBrokenAsVictim Сколько раз блокировки этого запроса были сняты

Запросы с высоким LocksBrokenAsBreaker — нарушители: именно они вызывают откаты других транзакций. Запросы с высоким LocksBrokenAsVictim — жертвы.

Анализ на уровне партиций

Для анализа сломанных блокировок на уровне партиций таблиц используйте следующие системные представления:

  • .sys/partition_stats — текущая статистика по партициям, содержит кумулятивное поле LocksBroken
  • .sys/top_partitions_by_tli_one_minute — топ-10 партиций с ненулевым числом сломанных блокировок за минутный интервал
  • .sys/top_partitions_by_tli_one_hour — топ-10 партиций с ненулевым числом сломанных блокировок за часовой интервал

Пример запроса для поиска партиций с наибольшим числом сломанных блокировок:

SELECT
    Path,
    SUM(LocksBroken) as TotalLocksBroken
FROM `.sys/partition_stats`
GROUP BY Path
ORDER BY TotalLocksBroken DESC
LIMIT 10;

Пример запроса для просмотра истории сломанных блокировок по партициям:

SELECT
    IntervalEnd,
    LocksBroken,
    Path
FROM `.sys/top_partitions_by_tli_one_hour`
WHERE IntervalEnd BETWEEN Timestamp("2000-01-01T00:00:00Z") AND Timestamp("2099-12-31T00:00:00Z")
ORDER BY IntervalEnd DESC, LocksBroken DESC
LIMIT 100;