Как переместить записи в одну транзакцию? Или шаблон Производитель-Потребитель в SQL - PullRequest
2 голосов
/ 11 октября 2011

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

У меня такое ощущение, что это что-то относительно простое, но я не уверен, что полностью понимаю, как работает блокировка в SQL. Это кажется супер тривиальным в C # (просто монитор), но SQL ...

В качестве примера вы можете рассмотреть модель «производитель-потребитель», то есть какая-то таблица, которая служит в качестве очереди, некоторые потоки вставляют в нее некоторые, которые потребляют. Вставка, по-видимому, не проблема, но меня интересует потребление.

UPDATE: Два хороших варианта решения для меня:

  • используйте SELECT FOR UPDATE (необходимо выяснить, как долго блокируются строки держать)
  • использовать поле для пометки записей перед манипуляцией с ними

И все же нужно выяснить, что Сериализуемое Ил вещь ...

Спасибо всем, кто приложил усилия и ответил - это сообщество так здорово.

Ответы [ 4 ]

3 голосов
/ 11 октября 2011

Редактировать 1: добавлена ​​одна небольшая заметка относительно ANSI_WARNINGS & ARITHABORT OFF.

Если вы используете SQL Server 2008 (я вижу, у вас есть вопросы относительно этой версии), вы можете попробовать компонуемый DML .

Простое решение:

INSERT  Target
SELECT  q.Id, q.Name, q.Type
FROM
(
        DELETE  Source 
        OUTPUT  deleted.Id, deleted.Name, deleted.Type
        WHERE   Type = @Type --or another search condition 
) q;

Сложный сценарий (включая ошибки):

1.Первый тестовый пример демонстрирует эту «технику».

2. Второй тест демонстрирует поведение при обнаружении ошибки во время выполнения оператора:оператор (INSERT + DELETE OUTPUT) отменен, но пакет все еще выполняется до последнего оператора.

3.В третьем тесте вы можете видеть, что ошибка может прервать «весь» пакет и оператор (INSERT + DELETE OUTPUT) также отменяется.

Поведение в отношении ошибок контролируется в этом сценарии с использованием трех настроек: ANSI_WARNINGS, ARITHABORT и XACT_ABORT .Если оба параметра (ANSI_WARNINGS и ARITHABORT) равны OFF, тогда это выражение 1/0 будет оценено как NULL =>, поэтому будет INSERT ... NULL.

SET NOCOUNT ON;

CREATE TABLE dbo.Source (Id INT PRIMARY KEY, Name VARCHAR(10) NOT NULL, Type TINYINT NOT NULL);
INSERT  dbo.Source (Id, Name, Type) VALUES (1,'A',1), (2, 'B',1), (3, 'C',2), (4, 'D',2), (5, 'E',2);

CREATE TABLE dbo.Target (Id INT PRIMARY KEY, Name VARCHAR(10) NOT NULL, Type TINYINT /*NOT*/ NULL);


    --***** Test 1 Ok *****
        DECLARE @Type INT = 1;

        SELECT  'Test 1 Ok' AS Description;
        BEGIN TRAN;
        INSERT  Target
        SELECT  q.Id, q.Name, q.Type
        FROM
        (
                DELETE  Source 
                OUTPUT  deleted.Id, deleted.Name, deleted.Type
                WHERE   Type = @Type
        ) q;

        SELECT  * FROM Target;
        SELECT  * FROM Source;
        --It will be fine to COMMIT transaction but I will cancel to run the second and third test
        ROLLBACK TRAN 
        SELECT  'End of Test 1 Ok' AS Description;
        GO
    --***** End of Test 1 *****

    --***** Test 2 Err *****
        --Start another batch
        GO 
        SET ARITHABORT ON;
        SET ANSI_WARNINGS ON;
        SET XACT_ABORT OFF;

        DECLARE @Type INT = 1;

        SELECT  'Test 2 Err' AS Description, SESSIONPROPERTY('ARITHABORT') [ARITHABORT_STATUS], SESSIONPROPERTY('ANSI_WARNINGS') [ANSI_WARNINGS_STATUS];

        INSERT  Target
        --Divide by zero => Abort statement only
        SELECT  q.Id, q.Name, CASE WHEN q.Id <> 2 THEN q.Type ELSE  1/0 END 
        FROM
        (
                DELETE  Source 
                OUTPUT  deleted.Id, deleted.Name, deleted.Type
                WHERE   Type = @Type
        ) q;

        SELECT  * FROM Target;
        SELECT  * FROM Source;
        SELECT  'End of Test 2 Err' AS Description;
    --***** End of Test 2 *****

    --***** Test 3 *****
        --Start another batch
        GO 
        SET ANSI_WARNINGS OFF;
        SET ARITHABORT ON;
        SET XACT_ABORT OFF;

        DECLARE @Type INT = 1;

        SELECT  'Test 3 Err' AS Description, SESSIONPROPERTY('ARITHABORT') [ARITHABORT_STATUS], SESSIONPROPERTY('ANSI_WARNINGS') [ANSI_WARNINGS_STATUS];

        INSERT  Target
        --Divide by zero => Abort batch
        SELECT  q.Id, q.Name, CASE WHEN q.Id <> 2 THEN q.Type ELSE  1/0 END
        FROM
        (
                DELETE  Source 
                OUTPUT  deleted.Id, deleted.Name, deleted.Type
                WHERE   Type = @Type
        ) q

        --This statement is not executed 
        SELECT  * , 1 AS Statement  FROM Target;
        --This statement is not executed 
        SELECT  * , 1 AS Statement FROM Source;
        --This statement is not executed 
        SELECT  'End of Test 3 Err' AS Description

        GO --Start another batch    
        SELECT  * , 2 AS Statement FROM Target;
        SELECT  * , 2 AS Statement FROM Source;
    --***** End of Test 3 *****

    DROP TABLE dbo.Source;
    DROP TABLE dbo.Target;
1 голос
/ 11 октября 2011

Используйте select for update для блокировки строк из исходной таблицы, скопируйте эти строки в таблицу назначения и затем удалите их.Другой поток, который выполняет ту же логику, будет ожидать вызова select for update.

1 голос
/ 11 октября 2011

некоторые предложения ...

  1. убедитесь, что в ваших таблицах есть правильные ключи или уникальные ограничения, чтобы дубликаты не могли быть вставлены

  2. Я бы использовал хранимую процедуру для массового перемещения (вставка + удаление) внутри транзакции начала / принятия. Я также хотел бы убедиться, что строки, выбранные для перемещения, сделаны с блокировкой на уровне строк. (Однако это может повлиять на производительность, если в этих таблицах много запросов выбора).

  3. в качестве альтернативы, вы могли бы фактически заблокировать код C #, который вызывает это действие, блокируя, чтобы гарантировать, что пользователь не может войти в метод вызова одновременно.

0 голосов
/ 11 октября 2011

Если вы используете SQL Server 2008, вы можете использовать MERGE

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...