【问题标题】:Concurrent read-modify-write in SQL ServerSQL Server 中的并发读-修改-写
【发布时间】:2015-07-03 21:14:11
【问题描述】:

我想用 SQL 做:

  • 第一步:从表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 锁,并且两个连接都将通过已经错误的第 1 步。但更糟的是,由于表上有 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 也需要并发证明)。 The TABLOCK also doesn't seems the best solution because when a new customer is selected, the existing customers (acquiring a ROWLOCK) are also waiting for the release of the TABLOCK.这就是我在上面提到“新行锁”的原因。

【问题讨论】:

  • 如果您首先对行进行虚假更新(例如 Amount=Amount),则其他选择将等待,具体取决于隔离级别。
  • 在表级别而不是事务上的锁定提示怎么样?看看here
  • 这就是 SNAPSHOT 隔离的用途。不过,您应该重新考虑该声明 - 它可以重写为单个 INSERT。一般来说,read-modify-write 是一种非常强烈的气味。总是有其他方法可以做同样的事情
  • 你可以写一个简单的INSERT .. SELECT 来插入你想要的值。为什么要将其分解为多个步骤?

标签: sql-server


【解决方案1】:

您可以在启动事务后在SELECT 上使用UPDLOCK / XLOCK 提示。 像这样。

示例表结构

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);

程序

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 的调用将并行工作而不会相互阻塞。

【讨论】:

  • 持有锁是并发证明的相反。引入 SNAPSHOT 隔离模式正是为了避免这个问题
  • OP 不想使用快照隔离,他希望其他事务等待
  • 这是基于几个不正确的假设,主要是您不能组合 INSERT 和 SELECT,或者必须一次处理一个记录。无论如何,延迟只会因为持有锁的时间更长而使事情变得更糟。可以通过将语句重写为简单的INSERT .. SELECT 来回答 OP 的问题。
  • 您不能将所有内容都放在一个 insert into select 中,因为根据 OP - “我不想将所有内容组合成一个语句(如果这可以解决任何问题),因为我的实际情况当然比例子复杂。”
  • @Panagiotis:快照隔离不是解决方案。有时它只是没有为数据库启用(在公司中),它很“重”,并且确实使用了我不想使用的乐观锁定(每次调用都需要成功)。此外,1 个大查询(组合插入/选择)或几个小查询没有任何区别。即使是单个语句也被隐式包装到事务中,默认情况下为 READ_COMMITED,这也无法解决我的问题(但是,它似乎在 99% 的时间都有效,因为如果不使用测试延迟之间的测试延迟,您将无法很好地测试并发读写)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-25
  • 1970-01-01
相关资源
最近更新 更多