在手动实现SQL Server数据库列的IDENTITY增量时避免竞争条件

Max*_*sky 2 c# sql-server asp.net-mvc race-condition linq-to-sql

我正在构建一个使用LINQ to SQL的ASP.NET MVC 2站点.在我的网站访问数据库的其中一个地方,我认为竞争条件是可能的.


数据库架构

以下是相关数据库表的一些列,命名为Revisions:

  • RevisionID - bigint,IDENTITY,PK
  • PostID - bigint,FK到Posts表的PK
  • EditNumber - int
  • RevisionText - nvarchar(max)

在我的网站上,用户可以提交帖子并稍后编辑帖子.原始海报以外的用户可以编辑帖子 - 因此可以同时在单个帖子上进行多次编辑.

提交帖子时,Posts会创建表格中的记录,以及Revisions表格中的记录,其中PostID设置为Posts记录的ID ,RevisionText设置为Post文本,EditNumber 设置为1.

当编辑一个帖子,只Revisions记录被创建,EditNumber被设定为比最新的编辑数量1高.

因此,EditNumber列指的是Post编辑的次数.


增加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();
}
Run Code Online (Sandbox Code Playgroud)

计算最大值并增加它可能会导致竞争条件.

有没有更好的方法来实现该功能?

Rem*_*anu 6

直接在数据库中更新并返回新版本:

update Revisions
set EditNumber += 1
output INSERTED.EditNumber
where PostID = @postId;
Run Code Online (Sandbox Code Playgroud)

不幸的是,这在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
Run Code Online (Sandbox Code Playgroud)

我省略了错误处理,请参阅模板过程的异常处理和嵌套事务,而不是正确处理错误和嵌套事务.

您会注意到Posts表有一个LastRevision字段,用作后修订的增量.这比每次添加修订时计算MAX要好得多,因为它避免了(范围)扫描修订.它还充当并发保护:一次只能有一个事务更新它,并且只有该事务才会继续插入新的修订.并发事务将阻塞并等待第一个提交,然后下一个未阻塞的事务将正确地将修订号更新为+1.