Как сделать эту транзакцию безопасной в параллельной среде? (SQL Server 2005) - PullRequest
1 голос
/ 12 августа 2009

Предположим, у меня есть две таблицы:

Invoice
-------
iInvoiceID int PK not null
dtCompleted datetime null

InvoiceItem
-----------
iInvoiceItemID int PK not null
iInvoiceID int FK (Invoice.iInvoiceID) not null
dtCompleted datetime null

Каждый InvoiceItem может быть выполнен другим процессом (исполняемым файлом), который выполняется на другой машине. Когда процесс будет завершен, я хочу, чтобы он вызывал хранимую процедуру, чтобы поставить отметку в поле InvoiceItem.dtCompleted, и я хочу, чтобы эта хранимая процедура возвращала флаг, указывающий, завершен ли весь счет. Какой бы процесс ни выполнялся, тот, кто завершил выставление счета, собирается запустить другой процесс, чтобы выполнить некоторую окончательную бизнес-логику над накладной, например, поставьте отметку в dtCompleted и отправьте квитанцию ​​по электронной почте. Очевидно, что я хочу, чтобы этот другой процесс запускался только один раз для данного счета.

Вот моя наивная реализация:

CREATE PROCEDURE dbo.spuCompleteInvoiceItem
    @iInvoiceItemID INT 
AS
BEGIN
    BEGIN TRAN

        UPDATE InvoiceItem 
        SET dtCompleted = GETDATE()
        WHERE iInvoiceItemID = @iInvoiceItemID

        IF EXISTS(SELECT * FROM InvoiceItem WHERE dtCompleted IS NULL 
                  AND iInvoiceID = (SELECT iInvoiceID FROM InvoiceItem
                                   WHERE iInvoiceItemID=@iInvoiceItemID))
            SELECT 'NotComplete' AS OverallInvoice
        ELSE
            SELECT 'Complete' AS OverallInvoice
    COMMIT
END

Достаточно ли этого? Или мне нужно повысить уровень сериализации транзакций и, если да, какой уровень обеспечит наилучший баланс производительности и безопасности?

Упреждающие комментарии:

  • Я знаю, что могу достичь той же бизнес-цели, внедрив центральную службу параллелизма на уровне процессов / исполняемых файлов, но я думаю, что это излишне. Мой инстинкт заключается в том, что, если я хорошо создаю свою хранимую процедуру и транзакцию, я могу использовать SQL Server в качестве службы межпроцессного параллелизма для этой простой операции, не сильно влияя на производительность или увеличивая частоту взаимоблокировок (пусть у меня будет торт и есть).
  • Я не беспокоюсь об обработке ошибок в этом примере. После этого я добавлю правильный материал TRY / CATCH / ROLLBACK / RAISERROR.

Обновление 1:

По мнению экспертов, мне нужен не только самый ограничительный уровень изоляции транзакции - сериализуемый - но мне также нужно заблокировать все InvoiceItems определенного счета-фактуры, прежде чем я сделаю что-либо еще, чтобы другие параллельные вызовы хранимая процедура будет блокироваться до завершения текущей. В противном случае я могу получить тупики. Вот моя последняя версия реализации:

CREATE PROCEDURE dbo.spuCompleteInvoiceItem
        @iInvoiceItemID INT 
    AS
    BEGIN
        IF @iInvoiceItemID IS NULL RAISERROR('@iInvoiceItemID cannot be null.', 16, 1)

        BEGIN TRAN

            SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

            DECLARE @iInvoiceID INT

            SELECT @iInvoiceID = iInvoiceID
            FROM InvoiceItem
            WHERE dtCompleted IS NULL 
            AND iInvoiceID = (SELECT iInvoiceID FROM InvoiceItem WHERE iInvoiceItemID=@iInvoiceItemID)

            IF @iInvoiceID IS NULL
            BEGIN
                -- Should never happen
                SELECT 'AlreadyComplete' AS Result
            END
            ELSE
            BEGIN
                UPDATE InvoiceItem SET dtCompleted = GETDATE() WHERE iInvoiceItemID = @iInvoiceItemID

                IF EXISTS(SELECT * FROM InvoiceItem WHERE iInvoiceID=@iInvoiceID AND dtCompleted IS NULL)
                    SELECT 'NotComplete' AS Result
                ELSE
                    SELECT 'Complete' AS Result
            END

        COMMIT

Спасибо

Джордан Ригер

Ответы [ 3 ]

1 голос
/ 12 августа 2009

У вас есть две альтернативы:

  1. Состояние с короткими транзакциями. Отметить статус обрабатываемых счетов. Задание выбирает счет-фактуру, подлежащий обработке, и обновляет его статус до «обработка» (выборочно обновляет), а затем фиксирует. Он обрабатывает счет, затем возвращается и обновляет статус «завершен». Не может быть другого задания, обрабатывающего тот же счет, потому что счет помечен как «обработка». Это типичный рабочий процесс на основе очередей.

  2. Без гражданства с длинными транзакциями. Найдите счет для обработки и заблокируйте его (UPDLOCK). На практике это делается путем полного обновления в начале транзакции, таким образом блокируя счет в режиме X. Держите транзакцию открытой, пока обрабатывается счет. В конце отметьте его как завершенное и введите.

Здесь ничто не поможет сделать уровни изоляции транзакций. Они влияют только на дуарцию и объем S-замков, а S-замки не могут предотвратить попытки двух заданий обработать один и тот же вызов, что приводит к блокировке и взаимоблокировкам.

Если «обработка» имеет любую длину, то вы должны использовать короткие транзакции с полным состоянием, поскольку удержание длинных блокировок транзакций в базе данных уничтожит все остальные действия. Недостаток заключается в том, что задания могут аварийно завершить работу без завершения обработки и оставить счета-фактуры в оставленном состоянии «обработка». Обычно это решается заданием «сбор мусора», которое сбрасывает статус обратно до «доступно», если они не завершаются в отведенное время.

Обновление

K. Тогда у запроса EXISTS должно быть предложение WHERE с InvoiceID, не так ли? Как и сейчас, он будет возвращать «Завершено», когда все позиции счета из всех счетов будут помечены полной датой.

В любом случае, эта последняя проверка на завершение является гарантированным тупиком на любом уровне изоляции: T1 обновляет элемент N-1 и выбирает СУЩЕСТВУЮЩИЕ. T2 обновляет элемент N и выбирает EXISTS. Блоки T1 при обновлении T2, блоки T2 при обновлении T1, взаимоблокировка. Никакой уровень изоляции здесь не поможет, и вероятный сценарий весьма . Никакой уровень изоляции не может предотвратить это, потому что причиной тупика является ранее существующее обновление, а не SELECT. В конечном итоге проблема вызвана тем, что параллельные процессоры вытесняют коррелированных элементов. До тех пор, пока вы позволите этому случиться, взаимоблокировки будут повседневным (или даже ежесекундным) фактом жизни с вашей обработкой. Я знаю это, потому что, как разработчик SQL Server в Редмонде, я провел большую часть последних 10 лет в этом проблемном пространстве. Вот почему Service Broker (встроенные очереди SQL Server) делают Блокировка группы разговоров : для изоляции обработки коррелированных сообщений. Если вы не убедитесь, что элементы из одного счета-фактуры обрабатываются только одним заданием, вы потратите остаток дня на решение новых сценариев тупиковой ситуации при обработке элементов. Лучшее, что вы можете сделать, - это создать очень ограничительную блокировку, которая блокирует весь счет-фактуру авансом, но на самом деле это именно то, о чем я вам говорю (блокировка доступа к элементам, связанным с ядром).

0 голосов
/ 01 января 2010

На мой взгляд, необходимо избегать любой необходимости сериализуемых / повторяемых операций чтения. Они являются результатом неправильного мышления о природе параллелизма в RDBMS и серьезно ограничивают масштабируемость. На многих платформах, включая Oracle, таких устройств просто не существует.

Я рекомендую проверить условие в операторе обновления.

UPDATE InvoiceItem  
        SET dtCompleted = GETDATE() 
        WHERE iInvoiceItemID = @iInvoiceItemID 
        **AND dtCompleted IS NULL**

IF (@@ROWCOUNT = 1)
...win

IF (@@ROWCOUNT = 0)
...loose

Если rowcount равен 1, то вы знаете, что обновление прошло успешно, и вы можете продолжить любую последующую постобработку, которую вам нужно сделать.

Если rowcount равен 0, то вы знаете, что либо проблема (InvoiceItem не существует), либо, скорее всего, что-то еще побеждает ваш процесс и задает dtCompleted, поэтому вы не должны продолжать.

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

0 голосов
/ 12 августа 2009

Ваша проблема здесь в том, что если два процесса одновременно завершают оставшиеся два элемента, они оба будут думать, что закончили счет.

Требуется блокировка всех других подробных позиций в счете-фактуре, чтобы гарантировать, что ничто другое не может изменить статус, пока процесс обновляет статус. Это, конечно, сократит конкуренцию, но это должно быть ограничено только текущим счетом и должно быть довольно коротким.

Вы можете сделать это, используя уровень изоляции SERIALIZABLE, чтобы гарантировать, что вы не можете получить фантомные чтения,


  DECLARE @iInvoiceId int

  BEGIN TRAN

  SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

  SET NOCOUNT ON

  -- This select locks the rows and ensures that repeated
  -- selects will produce the same result
  -- ie no other transaction can affect these rows,
  -- or insert a row into this invoice
  SELECT @iInvoiceId = iInvoiceId FROM InvoiceItem WITH (xlock)
  WHERE iInvoiceId = (
    SELECT iInvoiceId FROM InvoiceItem WHERE iInvoiceItemId = @iInvoiceItemId)

  SET NOCOUNT OFF
  -- perform request of query as before

  COMMIT
...