2015-06-02 4 views
2

я таблицу с именем SerialNumber в моей базе данных, которая выглядит следующим образом:Предотвращение одновременного выбора же следующего расчетного значения

[Id] [bigint] IDENTITY(1,1) NOT NULL, 
[WorkOrderId] [int] NOT NULL, 
[SerialValue] [bigint] NOT NULL, 
[SerialStatusId] [int] NOT NULL 

Где в моем приложении я всегда нужно получить MINSerialValue с SerialStatusId 1 для данный Рабочий заказ (WorkOrderId).
Если клиент выбрал определенный серийный номер, он должен немедленно изменить на SerialStatusId =2, чтобы следующий клиент не выбрал одно и то же значение.
Приложение установлено на нескольких станциях, которые могут одновременно обращаться к базе данных.
Я попробовал несколько подходов для решения этой проблемы, но без успеха:

update SerialNumber 
set SerialStatusId = 2 
output inserted.SerialValue 
where WorkOrderId = @wid AND 
SerialValue = (select MIN(SerialValue) FROM SerialNumber where SerialStatusId = 1) 

И

declare @val bigint; 
set @val = (select MIN(SerialNumber.SerialValue) from SerialNumber where SerialStatusId = 1 
and WorkOrderId = @wid); 

select SerialNumber.SerialValue from SerialNumber where SerialValue = @val; 
update SerialNumber set SerialStatusId = 2 where SerialValue = @val; 

И так я проверил, чтобы убедиться, что не выбирать одинаковые значения является:

public void Test_GetNext() 
    { 
     Task t1 = Task.Factory.StartNew(() => { getSerial(); }); 
     Task t2 = Task.Factory.StartNew(() => { getSerial(); }); 
     Task t3 = Task.Factory.StartNew(() => { getSerial(); }); 
     Task t4 = Task.Factory.StartNew(() => { getSerial(); }); 
     Task t5 = Task.Factory.StartNew(() => { getSerial(); }); 

     Task.WaitAll(t1, t2, t3, t4, t5); 


     var duplicateKeys = serials.GroupBy(x => x) 
        .Where(group => group.Count() > 1) 
        .Select(group => group.Key).ToList(); 
    } 

    private List<long> serials = new List<long>(); 
    private void getSerial() 
    { 
     object locker = new object(); 
     SerialNumberRepository repo = new SerialNumberRepository(); 
     while (serials.Count < 200) 
     { 
      lock (locker) 
      { 
       serials.Add(repo.GetNextSerialWithStatusNone(workOrderId)); 
      } 
     } 
    } 

Все тесты, которые я сделал до сих пор, дали ~ 70 дубликатов значений из таблицы, содержащей 200 серий. Как я могу улучшить свою инструкцию SQL, чтобы она не возвращала повторяющиеся значения?
UPDATE
Пытался осуществить то, что suggeted но @ Крис все еще получает количество дублируется:

SET XACT_ABORT, NOCOUNT ON 
DECLARE @starttrancount int; 
BEGIN TRY 
SELECT @starttrancount = @@TRANCOUNT 
IF @starttrancount = 0 BEGIN TRANSACTION 
update SerialNumber 
set SerialStatusId = 2 
output deleted.SerialValue 
where WorkOrderId = 35 AND 
SerialValue = (select MIN(SerialValue) FROM SerialNumber where SerialStatusId = 1) 
IF @starttrancount = 0 
COMMIT TRANSACTION 
ELSE 
EXEC sp_releaseapplock 'get_code'; 
END TRY 
BEGIN CATCH 
IF XACT_STATE() <> 0 AND @starttrancount = 0 
ROLLBACK TRANSACTION 
RAISERROR (50000,-1,-1, 'mysp_CreateCustomer') 
END CATCH 
+0

Re: update - связанный 'sp_getapplock' будет необходим до утверждения update. Обратите внимание, что это эффективно сериализует вызовы этого кода - я считаю, что вам не нужно быть столь же пессимистичным, как это, учитывая, что теоретически множественные параллельные вызовы должны быть доступны для разных '[WorkOrderId]'. Кроме того, я сделал опечатку в своем предыдущем ответе w.r.t. – StuartLC

ответ

2

Причина, почему вы получаете дубликаты, потому что есть race condition между временем предыдущее значение чтения , а новое значение обновляется до SerialNumber. Если два одновременных соединения (например, два разных потока) одновременно считывают одно и то же значение, они оба получат тот же новый SerialNumber. Второе соединение будет излишне обновлять статус снова.

Обновлено решение - предыдущая причина ТУПИКИ

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

CREATE PROC dbo.GetNextSerialLocked(@wid INT) 
AS 
BEGIN 
    BEGIN TRAN 
     DECLARE @NextAvailableSerial BIGINT; 
     SELECT @NextAvailableSerial = MIN(SerialValue) 
      FROM SerialNumber WITH (UPDLOCK, HOLDLOCK) where SerialStatusId = 1; 

     UPDATE SerialNumber 
     SET SerialStatusId = 2 
     OUTPUT inserted.SerialValue 
     WHERE WorkOrderId = @wid AND SerialValue = @NextAvailableSerial; 
    COMMIT TRAN 
END 
GO 

С приобретаемой + обновление теперь разделен на несколько заявлений, нам необходимо транзакцию для контроля срок службы HOLDLOCK.

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

Вы также можете рассмотреть альтернативные конструкции, чтобы избежать получения данных таким образом, например. если серийные номера должны увеличиваться, рассмотрите IDENTITY.

Также обратите внимание, что ваша блокировка кода на C# не имеет никакого эффекта, поскольку каждый Task будет иметь независимый объект locker, и никогда не будет конкуренции за ту же блокировку.

object locker = new object(); // <-- Needs to be shared between Tasks 
    SerialNumberRepository repo = new SerialNumberRepository(); 
    while (serials.Count < 200) 
    { 
     lock (locker) <-- as is, no effect. 

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

private ConcurrentBag<long> serials = new ConcurrentBag<long>(); 

Если вы хотите сериализовать доступ из C#, вам нужно будет использовать общий static object экземпляр во всех задачах/потоках (и, очевидно, сериализации из кода потребует, чтобы все доступ к серийному номеру поступал из одного и того же процесса). Сериализация в базе данных гораздо более масштабируемым, так как вы можете иметь несколько одновременных читателей для различных WorkOrderId

+0

1 для непрерывного усилия по улучшению ответа. ;) –

+0

Ну, у моей первой попытки была опечатка, и даже после исправления она вызвала взаимоблокировки - она ​​помогает фактически протестировать собственный код ;-) – StuartLC

+0

действительно пришел сюда, чтобы спросить о предыдущей версии вашего ответа: 'select MIN (SerialValue) FROM SerialNumber WITH (ROWLOCK, UPDLOCK, HOLDLOCK) '- как ROWLOCK/UPDLOCK работает в результате агрегирующего запроса? –

1

Посмотрите на ответ на этот вопрос: TSQL mutual exclusive access in a stored procedure.

Вам необходимо обеспечить взаимный эксклюзивный доступ к столу, в настоящее время вы просто полагаетесь на удачу (которая, как вы видели, не работает).

+0

Спасибо за ссылку, пытаясь реализовать это сейчас – Yoav

Смежные вопросы