Предотвращение состояния гонки при ручной реализации IDENTITY-подобного приращения для столбца БД SQL Server - PullRequest
2 голосов
/ 01 августа 2010

Я создаю сайт ASP.NET MVC 2, который использует LINQ to SQL. Я думаю, что в одном из мест, где мой сайт обращается к БД, возможно состояние гонки.


БД Архитектура

Вот некоторые из столбцов соответствующей таблицы БД с именем Revisions:

  • RevisionID - bigint, IDENTITY, PK
  • PostID - бигинт, ПК для ПК Posts таблица
  • EditNumber - int
  • RevisionText - nvarchar (max)

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

При отправке сообщения создается запись в таблице Posts, а также запись в таблице Revisions с PostID , установленным на идентификатор записи Posts, RevisionText установлен в текст сообщения, а EditNumber установлен в 1.

При редактировании сообщения создается только запись Revisions, с EditNumber , установленным на 1 больше, чем последний номер редактирования .

Таким образом, столбец EditNumber указывает, сколько раз редактировалось сообщение.


Увеличение EditNumber

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

Вот мой запрос LINQ для определения того, какой EditNumber должна иметь новая ревизия:

using(var db = new DBDataContext())
{
    var rev = new Revision();
    rev.EditNumber = db.Revisions.Where(r => r.PostID == postID).Max(r => r.EditNumber) + 1;

    // ... (fill other properties)

    db.Revisions.InsertOnSubmit(rev);
    db.SubmitChanges();
}

Вычисление максимума и его увеличение может привести к состоянию гонки .

Есть ли лучший способ реализовать эту функцию?

Ответы [ 5 ]

6 голосов
/ 01 августа 2010

Обновление непосредственно в базе данных и возврат новой версии:

update Revisions
set EditNumber += 1
output INSERTED.EditNumber
where PostID = @postId;

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

Обновлен:

Вот как я могу вставить новую ревизию (включая первую ревизию):

create procedure usp_insertPostRevision
  @postId int,
  @text nvarchar(max),
  @revisionId bigint output

as 
begin
  set nocount on;
  declare @nextEditNumber (EditNumber int not null);
  declare @rc int = 0;

  begin transaction;
  begin try
    update Posts
    set LastRevision += 1
    output INSERTED.LastRevision
       into @nextEditNumber (EditNumber)
    where PostId = @postId;

    set @rc = @@rowcount;

    if (@rc <> 1)
      raiserror (N'Expected exactly one post with Id:%i. Found:%i', 
        16, 1 , @postId, @rc);

    insert into Revisions
      (PostId, Text, EditNumber)
    select @postID, @text, EditNumber
    from @nextEditNumber;

    set @revisionId = scope_identity();

    commit;    
  end try
  begin catch
   ... // Error handling omitted
  end catch
end

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

Вы заметите, что в таблице Posts есть поле LastRevision, которое используется в качестве приращения для редакций записей. Это гораздо лучше, чем вычислять MAX каждый раз, когда вы добавляете ревизию, поскольку это позволяет избежать (диапазона) сканирования ревизий. Он также действует как защита от параллелизма: только одна транзакция за раз сможет его обновить, и только эта транзакция будет продолжать вставлять новую ревизию. Параллельные транзакции будут блокироваться и ждать, пока первая транзакция не завершится, затем следующая разблокированная транзакция корректно обновит номер редакции до + 1.

2 голосов
/ 01 августа 2010

Могут ли несколько пользователей редактировать одно и то же сообщение одновременно? Если нет, то у вас нет условия гонки, если только один пользователь не может отправить несколько правок одновременно.

1 голос
/ 01 августа 2010

Поскольку в таблице «Сообщения» имеется только одна запись, используйте блокировку.

Прочитайте запись в таблице Posts и используйте табличную подсказку [WITH (ROWLOCK, XLOCKX)] для получения эксклюзивной блокировки. Установите время ожидания блокировки на несколько миллисекунд.

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

1 голос
/ 01 августа 2010

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

0 голосов
/ 01 августа 2010

Поскольку EditNumber - это свойство, определяемое членством в коллекции, пусть коллекция предоставит его.

Сделать EditNumber вычисляемым столбцом - COUNT записей для того же поста с меньшим RevisionID.

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