并发访问数据库 - 阻止两个用户获取相同的值

时间:2012-03-19 01:25:07

标签: c# sql-server entity-framework-4 concurrency

我有一个包含序号的表格(想想发票号码或学生证)。

在某些时候,用户需要请求前一个号码(以便计算下一个号码)。一旦用户知道当前号码,他们就需要生成下一个号码并将其添加到表格中。

我担心的是,由于并发访问,两个用户将能够错误地生成两个相同的号码。

我听说过存储过程,我知道这可能是一个解决方案。这里有最好的做法,以避免并发问题吗?

修改:这是我到目前为止所拥有的:

USE [master]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[sp_GetNextOrderNumber]
AS
BEGIN
    BEGIN TRAN

DECLARE @recentYear INT
DECLARE @recentMonth INT
DECLARE @recentSequenceNum INT

-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;

    -- get the most recent numbers
SELECT @recentYear = Year, @recentMonth = Month, @recentSequenceNum = OrderSequenceNumber
FROM dbo.OrderNumbers 
WITH (XLOCK)
WHERE Id = (SELECT MAX(Id) FROM dbo.OrderNumbers)

    // increment the numbers
IF (YEAR(getDate()) > IsNull(@recentYear,0))
    BEGIN
        SET @recentYear = YEAR(getDate());
        SET @recentMonth = MONTH(getDate());
        SET @recentSequenceNum = 0;
    END
ELSE
    BEGIN
        IF (MONTH(getDate()) > IsNull(@recentMonth,0))
            BEGIN
                SET @recentMonth = MONTH(getDate());
                SET @recentSequenceNum = 0;
            END
        ELSE
            SET @recentSequenceNum = @recentSequenceNum + 1;
    END 

-- insert the new numbers as a new record   
INSERT INTO dbo.OrderNumbers(Year, Month, OrderSequenceNumber)
VALUES (@recentYear, @recentMonth, @recentSequenceNum)

COMMIT TRAN
END

这似乎有用,并且给了我想要的价值。 到目前为止,我还没有添加任何锁定来防止并发访问。

编辑2 :添加WITH(XLOCK)以锁定表,直到事务完成。我不打算在这里表演。只要我没有添加重复的条目,并且没有发生死锁,这应该可行。

3 个答案:

答案 0 :(得分:6)

你知道SQL Server会为你做这件事,对吧?如果需要序列号或计算列,如果需要根据另一个计算新值,可以使用标识列。

但是,如果这不能解决您的问题,或者您需要进行复杂的计算来生成无法在简单插入中完成的新数字,我建议编写一个锁定表的存储过程,获取最后一个值,生成新值,插入它然后解锁表。

阅读此link以了解事务隔离级别

确保尽可能小的“锁定”时间

答案 1 :(得分:3)

以下是Counter实施示例。基本思路是使用插入触发器来更新发票的数量,发票。第一步是创建一个表来保存最后分配的数字的值:

create table [Counter]
(
   LastNumber int
)

并用单行初始化它:

insert into [Counter] values(0)

样本发票表:

create table invoices 
(
  InvoiceID int identity primary key, 
  Number varchar(8), 
  InvoiceDate datetime
)

存储过程LastNumber首先更新Counter行,然后检索该值。由于该值是一个int,它只是作为过程返回值返回;否则将需要输出列。过程作为下一个数字的参数号来获取;输出是最后一个数字。

create proc LastNumber (@NumberOfNextNumbers int = 1)
as
begin
   declare @LastNumber int

   update [Counter] 
      set LastNumber = LastNumber + @NumberOfNextNumbers -- Holds update lock
   select @LastNumber = LastNumber 
     from [Counter]
   return @LastNumber 
end

Invoice表上的触发器获取同时插入的发票的数量,从存储过程中询问下n个数字并更新具有该数字的发票。

create trigger InvoiceNumberTrigger on Invoices
after insert
as
   set NoCount ON

   declare @InvoiceID int
   declare @LastNumber int
   declare @RowsAffected int

   select @RowsAffected = count(*)
     from Inserted
   exec @LastNumber = dbo.LastNumber @RowsAffected

   update Invoices
    -- Year/month parts of number are missing
      set Number = right ('000' + ltrim(str(@LastNumber - rowNumber)), 3)
     from Invoices
        inner join
        ( select InvoiceID, 
                 row_number () over (order by InvoiceID desc) - 1 rowNumber
            from Inserted
        ) insertedRows
          on Invoices.InvoiceID = InsertedRows.InvoiceID

如果发生回滚,则不会留下任何空隙。可以使用不同序列的键轻松扩展计数器表;在这种情况下,date-until可能很好,因为你可能事先准备好这个表,让LastNumber担心选择当前年/月的计数器。

使用示例:

insert into invoices (invoiceDate) values(GETDATE())

当数字列的值自动生成时,应该重新读取它。我相信EF有这方面的规定。

答案 2 :(得分:0)

我们在SQL Server中处理此问题的方法是在单个事务中使用UPDLOCK表提示。

例如:

  INSERT 
    INTO MyTable (
         MyNumber ,
         MyField1 )
  SELECT IsNull(MAX(MyNumber), 0) + 1 ,
         "Test"
    FROM MyTable WITH (UPDLOCK)

它并不漂亮,但由于我们提供了数据库设计,并且由于遗留应用程序访问数据库而无法更改它,因此这是我们可以提出的最佳解决方案。