【发布时间】: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