Практика SQL Server

Ошибки транзакций SQL Server: откат и передача ошибки

Используйте TRY CATCH, XACT_ABORT и XACT_STATE с явным владельцем транзакции, чтобы избежать частичных изменений и ложного сообщения об успехе.

Операция, уменьшившая остаток товара, но не создавшая резерв, не является частично успешной. Она нарушила бизнес-инвариант. SQL Server позволяет сделать записи атомарными, если границы транзакции и обработка ошибок согласованы. CATCH, который печатает сообщение и нормально возвращается, способен создать опасный ложный успех.

Сначала определите успешное состояние. Уменьшение остатка и создание резерва должны либо подтвердиться вместе, либо вместе исчезнуть. Пример сам владеет транзакцией и отказывается работать внутри уже открытой внешней. Для переиспользуемой процедуры в более крупной операции необходим другой явный контракт.

Ошибка после первой записи

Создайте временные таблицы в одном учебном соединении.

CREATE TABLE #Stock (ProductId int PRIMARY KEY, Quantity int NOT NULL);
CREATE TABLE #Reservations (
    ReservationId int PRIMARY KEY, ProductId int NOT NULL, Quantity int NOT NULL
);
INSERT #Stock VALUES (1, 10);
INSERT #Reservations VALUES (1, 1, 1);

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

SET XACT_ABORT ON;
IF @@TRANCOUNT <> 0
    THROW 50001, 'Run without an existing transaction.', 1;

DECLARE @ReservationId int = 1; -- Deliberate duplicate for the failure test.
DECLARE @Quantity int = 3;
BEGIN TRY
    BEGIN TRANSACTION;
    UPDATE #Stock
    SET Quantity = Quantity - @Quantity
    WHERE ProductId = 1 AND Quantity >= @Quantity;
    IF @@ROWCOUNT <> 1
        THROW 50002, 'Insufficient stock or missing product.', 1;

    INSERT #Reservations(ReservationId, ProductId, Quantity)
    VALUES (@ReservationId, 1, @Quantity);
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
    THROW;
END CATCH;

Условный UPDATE проверяет и меняет доступность одной инструкцией. Отдельное незащищённое чтение остатка позволило бы конкурентным сеансам принимать решения по устаревшим данным. Рабочий код также обязан отклонять нулевые и отрицательные количества; фиксированное положительное значение упрощает демонстрацию.

После ошибки INSERT предыдущее уменьшение должно откатиться. Выполните проверку отдельно в том же соединении после ожидаемой ошибки.

-- Run separately after the expected error, in the same connection.
SELECT ProductId, Quantity FROM #Stock;
SELECT @@TRANCOUNT AS OpenTransactions, XACT_STATE() AS TransactionState;
-- Expected: Quantity = 10, OpenTransactions = 0, TransactionState = 0.

Количество должно остаться равным 10, открытой транзакции быть не должно. Для успешного сценария начните со свежих данных и нового идентификатора, например 2: остаток станет 7. Одна проверка сообщения об ошибке не показывает, сохранилась ли первая запись.

Три механизма решают разные задачи

TRY CATCH перехватывает многие ошибки выполнения, но не все возможные сбои. Некоторые ошибки компиляции того же уровня, завершение соединения и отмена клиентом требуют обработки у вызывающей стороны. Процедура не может обещать нормальный ответ через уже исчезнувшее соединение.

SET XACT_ABORT ON заставляет многие ошибки выполнения прерывать транзакцию, а не только отдельную инструкцию. Для атомарной записи это полезно, но не заменяет явную очистку и передачу ошибки. THROW учитывает XACT_ABORT; RAISERROR ведёт себя иначе и не является просто другой записью той же команды.

XACT_STATE различает отсутствие транзакции, состояние с возможностью подтверждения и состояние без такой возможности. @@TRANCOUNT показывает вложенность, а не допустимость COMMIT. В данном шаблоне любая оставшаяся транзакция откатывается, потому что бизнес-операция целиком не удалась, даже если технически подтверждение ещё возможно.

THROW без аргументов внутри CATCH сохраняет исходную ошибку. Замена её успешным возвратом или общей новой ошибкой затрудняет диагностику и выбор повторов. Для телеметрии полезны ERROR_NUMBER, ERROR_PROCEDURE и ERROR_LINE вместе с идентификатором запроса.

Не нарушайте контракт вызывающего кода

Обычный ROLLBACK без точки сохранения откатывает всю транзакцию, включая предыдущую работу вызывающей стороны. Не переносите этот шаблон без изменений во вложенную вспомогательную процедуру. Внутренние BEGIN TRANSACTION и COMMIT не создают независимо сохраняемых вложенных транзакций.

Комбинируемая процедура может запомнить входной счётчик транзакций и при необходимости создать savepoint. Однако транзакцию, которую нельзя подтвердить, невозможно исправить откатом к этой точке: владелец должен отменить её полностью. Распределённые транзакции также ограничивают поддержку точек сохранения. Простая явная ответственность часто надёжнее внешне универсального обработчика.

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

Сетевой тайм-аут около COMMIT означает неопределённый результат, а не доказанный откат. Для небезопасных повторов используйте идентификаторы запросов и проверку авторитетного статуса. Проверьте отказ после каждой записи, успешный путь и реакцию клиента. Отдельно убедитесь, что пул соединений не получает сеанс с неожиданно открытой транзакцией после обработанного сбоя. Пользователь приложения не должен видеть успех после фактического отката.

Техническая документация: Microsoft Learn: SET XACT_ABORT · Microsoft Learn: TRY CATCH · Microsoft Learn: XACT_STATE.

Вопрос по статье

Есть вопрос по этой теме?

Расскажите, что вы оцениваете или с какой проблемой столкнулись. Мы ответим с практической рекомендацией.

Inquiries are not enabled in this preview.

Задать вопрос по этой статье