SQL Server

时间:2015-07-03 06:21:16

标签: sql-server

我想用SQL做:

  • 步骤1:从表A,B,C
  • 读取数据
  • 第2步:做一些计算
  • 将数据写回表A,B,C

但这必须是并发证明,这意味着一旦完成第1步,每个其他实例都需要等待第1步,直到第3步结束,因为它更改了数据以进行计算。这是一个简化的例子(我省略了一些声明并使用了硬编码值):

CREATE PROCEDURE AddOrder AS BEGIN
    -- Step 1: read (every other call to AddOrder should wait here until this procedure has finished
    SELECT @TotalOrderAmount = sum(Amount) FROM Orders WHERE CustomerID = 5

    -- Step 2: modify
    SELECT @DiscountPct = CASE WHEN @TotalOrderAmount > 1000.00 THEN 0.10 ELSE 0.00 END
    SELECT @Amount = 9.99 * (1 - @DiscountPct)

    -- Step 3: write
    INSERT INTO Orders(CustomerID, Amount) VALUES (5, @Amount)
END

我脑海中的第一件事当然是使用具有提升的隔离级别的事务:

SET TRANSACTION ISOLATION LEVEL REPEATBLE READ
BEGIN TRAN
    -- Step 1
    -- Step 2
    -- Step 3
COMMIT TRAN

但这不会解决任何问题。假设2个连接在完全相同的时间执行该过程。步骤1将放置并保持一个shared_read锁,并且两个连接都将通过已经错误的第一步。但它变得更糟,因为表中有2个锁将在步骤3中更新,会出现死锁。

我不想将所有内容分组到一个语句中(如果这样可以解决任何问题),因为我的实际情况当然比示例更复杂。

我还想使用现代SQL Server的范围锁定而不是锁定整个表,以便只锁定该CustomerID的行。 最后,我不会乐观锁定,因此两个调用应该总是成功。

有没有人有这个问题的简单解决方案?

更新:

首先,似乎使用表提示UPDLOCK可以解决问题。例如:

    BEGIN TRAN
        -- Step 1: read or wait until other instance has finished
        SELECT @TotalOrderAmount = sum(Amount) FROM Orders with (UPDLOCK, ROWLOCK) WHERE CustomerID = 5

        -- Step 2: modify
        SELECT @DiscountPct = CASE WHEN @TotalOrderAmount > 1000.00 THEN 0.10 ELSE 0.00 END
        SELECT @Amount = 9.99 * (1 - @DiscountPct)

        -- Step 3: write
        INSERT INTO Orders(CustomerID, Amount) VALUES (5, @Amount)
    COMMIT TRAN

最大的好处是只有CustomerID = 5的订单行才会被锁定,因此大多数通话都不会等待,因为它们会与不同的客户一起使用。

但是这种方法仍然存在一个主要缺点:对于新客户来说它根本不起作用,因为还没有行可以锁定。因此,具有相同新CustomerID(尚未订购)的2个并发调用将不会彼此等待。

所以除了UPDLOCK,ROWLOCK我还需要像

这样的东西
  • 如果范围存在,请执行ROWLOCK
  • 如果范围不存在,请执行TABLOCK(或类似'新行锁定')

这样的东西
BEGIN TRAN
    IF EXISTS(SELECT * FROM Orders WHERE CustomerID = 5)
        SELECT @TotalOrderAmount = sum(Amount) FROM Orders with (UPDLOCK, ROWLOCK) WHERE CustomerID = 5
    ELSE
        SELECT @TotalOrderAmount = sum(Amount) FROM Orders with (UPDLOCK, TABLOCK) WHERE CustomerID = 5

但是在1声明中(因为IF EXISTS也需要并发证明)。 TABLOCK似乎也不是最好的解决方案,因为当选择新客户时,现有客户(获得ROWLOCK)也在等待TABLOCK的发布。这就是我在上面提到'新行锁'的原因。

1 个答案:

答案 0 :(得分:1)

开始交易后,您可以在UPDLOCK上使用XLOCK / SELECT提示。  这样的事情。

样本表结构

CREATE TABLE Orders
(
    OrderID INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    CustomerID INT NOT NULL,
    Amount NUMERIC(18,2) NOT NULL
);

CREATE INDEX IDX_Cutomer_Orders ON Orders(CustomerID) INCLUDE(Amount);

INSERT INTO Orders VALUES(1,123.25),(1,55),(2,8765900),(7,900);

INSERT INTO Orders VALUES(5,123.25),(5,8765900);

<强> PROCEDURE

CREATE PROCEDURE AddOrder
@CustomerID INT
AS
BEGIN

    BEGIN TRANSACTION

    DECLARE @TotalOrderAmount NUMERIC(18,2),@Amount NUMERIC(18,2),@DiscountPct NUMERIC(4,2)
    -- Step 1: read (every other call to AddOrder should wait here until this procedure has finished
    SELECT @TotalOrderAmount = SUM(Amount) FROM Orders WITH (UPDLOCK ,ROWLOCK)
    WHERE CustomerID = @CustomerID

    -- Step 2: modify
    SELECT @DiscountPct = CASE WHEN @TotalOrderAmount > 1000.00 THEN 0.10 ELSE 0.00 END
    SELECT @Amount = 9.99 * (1 - @DiscountPct)

    WAITFOR DELAY '00:00:10'
    -- Step 3: write
    INSERT INTO Orders(CustomerID, Amount) VALUES (@CustomerID, @Amount)

    SELECT * FROM Orders WHERE CustomerID = @CustomerID

    COMMIT
END

此处,同时拨打EXEC AddOrder 1会等待初始的commit / rollback

EXEC AddOrder 1EXEC AddOrder 5的通话将并行工作而不会相互阻挡。