4

考虑到我有一笔交易:

BEGIN TRANSACTION
DECLARE MONEY @amount
SELECT Amount AS @amount
  FROM Deposits
  WHERE UserId = 123
UPDATE Deposits
  SET Amount = @amount + 100.0
  WHERE UserId = 123
COMMIT

它在 2 个线程上执行,顺序为:

  1. 线程 1 - 选择
  2. 线程 2 - 选择
  3. 线程 1 - 更新
  4. 线程 2 - 更新

假设执行前Amount为0。

在这种情况下,在 SQL Server 的不同设置(未提交读取、已提交读取、可重复读取、可序列化)下会发生什么情况,最后会发生什么数量,会不会出现死锁?

4

5 回答 5

2

其他人已经解决了使用 REPEATABLE READ 的问题。

所以我会提出不同的建议......

为什么要使用两个语句而不是像下面这样的一个语句?

UPDATE Deposits
SET Amount = Amount + 100.0
WHERE UserId = 123

此外,您的真实交易发生的不仅仅是用户 ID,对吗?如果没有,您将面临使用比最初预期更多的记录的风险。

于 2008-11-03T17:59:15.083 回答
2

很好的陈述场景。我决定测试一下。

这是我的设置脚本:

CREATE TABLE Deposits(Amount Money, UserID int)
INSERT INTO Deposits (Amount, UserID)
SELECT 0.0, 123
--Reset
UPDATE Deposits
SET Amount = 0.00
WHERE UserID = 123

这是我的测试脚本。

SET TRANSACTION ISOLATION LEVEL Serializable
----------------------------------------
-- Part 1
----------------------------------------
BEGIN TRANSACTION
DECLARE @amount MONEY
SET @amount =
(
SELECT Amount
FROM Deposits
WHERE UserId = 123
)
SELECT @amount as Amount
----------------------------------------
-- Part 2
----------------------------------------
DECLARE @amount MONEY
SET @amount =  *value from step 1*
UPDATE Deposits
SET Amount = @amount + 100.0
WHERE UserId = 123
COMMIT
SELECT *
FROM Deposits
WHERE UserID = 123

我在两个查询分析器窗口中加载了这个测试脚本,并按照问题的描述运行了每个部分。

所有读取都发生在任何写入之前,因此所有线程/场景都会将 0 的值读入@amount。

结果如下:

读已提交

1 T1.@Amount = 0.00
2 T1.@Amount = 0.00
3 Deposits.Amount = 100.00
4 Deposits.Amount = 100.00

读取未提交

1 T1.@Amount = 0.00
2 T1.@Amount = 0.00
3 Deposits.Amount = 100.00
4 Deposits.Amount = 100.00

可重复读取

1 T1.@Amount = 0.00 (locks out changes by others on Deposit.UserID = 123)
2 T1.@Amount = 0.00 (locks out changes by others on Deposit.UserID = 123)
3 Hangs until step 4. (due to lock in step 2)
4 Deadlock!
Final result: Deposits.Amount = 100.00

可序列化

1 T1.@Amount = 0.00 (locks out changes by others on Deposit)
2 T1.@Amount = 0.00 (locks out changes by others on Deposit)
3 Hangs until step 4. (due to lock in step 2)
4 Deadlock!
Final result: Deposits.Amount = 100.00

以下是每种类型的解释,可用于通过思维模拟来达到这些结果。

Read CommittedRead Uncommited都不会锁定已读取的数据以防止其他用户修改。不同之处在于,未提交的读取将允许您查看尚未提交的数据(不利),并且如果有数据被其他人锁定以防止读取(有利),则不会阻止您的读取,这实际上是在说两次相同的事情。

Repeatable ReadSerializable,两者都表现得像 read commited for reading。对于锁定,两者都锁定已读取的数据以防止其他用户修改。不同之处在于可序列化的阻塞比已读取的行多,它还阻塞会引入以前不存在的记录的插入。

因此,通过可重复读取,您可以在以后的读取中看到新记录(称为:幻像记录)。使用可序列化,您可以阻止这些记录的创建,直到您提交为止。

以上解释来自我对这篇msdn文章的解读。

于 2008-11-03T18:25:01.880 回答
1

是的,您可能想要可重复阅读。

我可能会通过乐观锁定来处理这个问题,其中只有在现有值与您读取时的值相同时才更新(测试和设置)。如果值不同,则引发错误。这允许您以未提交的方式运行,没有死锁,也没有数据损坏。

BEGIN TRANSACTION
DECLARE MONEY @amount
SELECT Amount AS @amount
  FROM Deposits
  WHERE UserId = 123
UPDATE Deposits
  SET Amount = @amount + 100.0
  WHERE UserId = 123 AND Amount = @amount
IF @@ROWCOUNT <> 1 BEGIN ROLLBACK; RAISERROR(...) END
ELSE COMMIT END
于 2008-11-03T17:45:10.963 回答
1

否则,您可以使用锁定提示来避免死锁(如果您的服务器处于读取提交模式):

BEGIN TRANSACTION
DECLARE MONEY @amount
SELECT Amount AS @amount
  FROM Deposits WITH(UPDLOCK)
  WHERE UserId = 123
UPDATE Deposits
  SET Amount = @amount + 100.0
  WHERE UserId = 123
COMMIT

当然,在这个特定的过程中,单个语句(如 Kevin Fairchild 发布的)是首选并且不会引起副作用,但在更复杂的情况下,UPDLOCK 提示可能会变得很方便。

于 2008-11-03T18:31:00.323 回答
0

我相信你会想要使用可重复读取,它会锁定记录,第一个选择会获取值,然后它会更新阻塞线程二,直到它完成。因此,在您的示例中,最终结果为 200

未提交读取将导致两条记录都将值设置为 100。

已提交的读取可能会产生一些有趣的结果,具体取决于两个线程的时间......

这是我发现的一篇关于可重复读取的好文章,它提供了一个很好的例子

于 2008-11-03T16:03:19.547 回答