SQL 更新中的服务器并发

SQL Server Concurrency in update

我有一个 TABLE:

id  status  mod runat
1   0   null    null
2   0   null    null
3   0   null    null
4   0   null    null

而且,我同时两次调用此查询。

UPDATE TABLE
SET
    status = 1,
    mod = GETDATE()
OUTPUT INSERTED.id
WHERE id = (
        SELECT TOP (1) id
        FROM TABLE 
        WHERE STATUS = 0
            AND NOT EXISTS(SELECT * FROM TABLE WHERE STATUS = 1)
            AND COALESCE(runat, GETDATE()) <= GETDATE()
        ORDER BY ID ASC)

而且...有时我有:

1
1

相反

1
NULL

为什么?更新查询不是事务性的?

简答

将 WITH (UPDLOCK, HOLDLOCK) 添加到 select

UPDATE TABLE
SET
    status = 1,
    mod = GETDATE()
OUTPUT INSERTED.id
WHERE id = (
        SELECT TOP (1) id
        FROM TABLE WITH (UPDLOCK, HOLDLOCK)
        WHERE STATUS = 0
            AND NOT EXISTS(SELECT * FROM TABLE WHERE STATUS = 1)
        AND COALESCE(runat, GETDATE()) <= GETDATE()
    ORDER BY ID ASC)

说明

因为您使用子查询来获取 ID,所以这里基本上有两个语句 运行 - select 和一个更新。当 1 返回两次时,它仅表示在任一更新完成之前都 select 语句 运行 。如果您添加一个 UPDLOCK,那么当第一个 运行s 它持有 UPDLOCK。第二个 SELECT 必须等待第一个 select 释放 UPDLOCK 才能执行。

更多信息

究竟发生了什么取决于数据库的锁定方案,以及其他语句发出的锁。这种更新在某些情况下甚至会导致死锁。

因为语句 运行 太快了,很难看出它们持有什么锁。要有效地减慢速度,一个好技巧是

  1. 打开一个会话,运行 第一个语句带有 BEGIN TRANS 它开头的语句(不包括 COMMIT 或 ROLLBACK)
  2. 运行 在 sys.dm_tran_locks 上查询以查看正在持有的锁
  3. 打开第二个会话和运行第二个语句,看看是什么 发生。如果您的锁定方案设置正确,它应该等待 第一个在它做任何事情之前完成。
  4. 切换回第一个会话并提交以模拟完成

link 包含很多信息,但锁定和数据争用是复杂的领域,有很多可能的解决方案。 link 应该为您提供决定如何处理此问题所需的一切信息。 https://docs.microsoft.com/en-us/sql/relational-databases/sql-server- t运行saction-locking-and-row-versioning-guide?view=sql-server-2017