这是 'thread-safe' 更新和 return 队列中 SQL 行的方法吗?

Is this a 'thread-safe' way to update and return a row in SQL from a queue?

我在 SQL 中实现了一个队列,方法是让 table 有一个 'claimedby' 列。许多进程中的任何一个都可以通过填充 claimedby 字段从该队列中获取一个项目。

我想做的是防止两个进程抓取同一个项目。

我做了一些研究,发现了一些实现此目的的可能性。我不想使用代理,我想坚持使用简单的 table.

我正在使用 C# 处理检索到的项目。

我一直在SQL玩,写了以下内容:

declare @test table (ID int);

UPDATE TOP (1) CatQueueItem SET ClaimedBy = 'Mittens'
OUTPUT inserted.CatQueueItemIdId into @test
WHERE ClaimedBy is null

这将在 C# 中作为单个查询完成,方法是将参数添加为输出参数等。

我想知道此代码是否有效,或者我是否需要考虑锁定和事务处理以确保如果多个进程同时 运行 此查询将按预期运行(只有一个进程会声明该项目,其他更新将完全跳过这一行,因为它是由第一个更新的)。

有什么想法吗?

SQL 服务器中的每个查询都在隐式事务中运行。由于您在这里使用单个语句(而不是多个查询),引擎将为您处理锁定和阻塞。

只要您处理的是 C# 代码中没有更新记录的情况,这应该可以很好地处理并发。

虽然由于创建了隐式事务,给定的查询不会将行提供给多个线程,但它可能会导致一个线程设法占用队列的问题。如果要停止此操作,您可能需要向查询添加 READ PAST 和 ROW LOCK 提示。这可以防止一个线程获得的锁阻止其他线程获得一行。

例如:

UPDATE TOP (1) CatQueueItem WITH (READPAST, ROWLOCK)
SET ClaimedBy = 'Mittens'
OUTPUT inserted.CatQueueItemIdId into @test
WHERE ClaimedBy is null

详细解释

SQL 服务器 运行 中的所有语句在事务的上下文中。如果语句是 运行 并且没有指定事务 SQL 服务器仅为该单个语句创建一个隐式事务。

SQL服务器主要使用共享(S)、更新(U)和独占(X)三种锁类型。锁是在事务范围内获取的。如果一个事务在一行上有 S 锁,其他事务可以获得 S 或 U 锁,但不能在同一行上获得 X 锁。如果一个事务在一行上有 U 锁,其他事务只能在该行上获得 S 锁。如果一个事务在某行上有 X 锁,则其他事务无法获得该行上的锁。为了写入一行,一个事务需要有一个 X 锁,因为这会阻止所有其他事务在更新它的过程中读取该行。

这提供了以下兼容性 table:

    S  U  X
  ----------
S | Y  Y  N
U | Y  N  N
X | N  N  N

问题中的更新有两个部分。首先,它读取 table 以查找在 ClaimedBy 中具有空值的行。一旦找到该行,操作的第二部分就会更新找到的行。

通常从 tables 读取时 SQL 服务器使用 S 锁,因为这些不会阻止其他事务也读取行并提高读取性能,但它确实会阻止其他事务获得 X锁定以写入行。这样做的问题是,当更新查询的第二部分尝试升级到 X 锁以便它可以写入该行时,它可能会导致死锁。这样做的原因是另一个事务中查询的第一部分可能已经获得了与您的事务相同的 S 锁,但可能还没有升级它。这会阻止您的事务将其锁升级为 X 锁,而您的 S 锁也会阻止其他事务升级。两个事务都无法成功,因此它们陷入僵局。在这种情况下 SQL 服务器选择一个事务并将其回滚。

为了阻止死锁的发生,在执行更新语句的读取部分时SQL 服务器使用 U 锁。 U 锁允许其他事务获取 S 锁,允许那些正在执行读取的事务成功,但它不允许其他 U 锁。通过使用 U 型锁,您表示您只是在阅读,但您打算在将来的某个时间点进行写作。这可以防止出现两个事务都试图升级到 X 锁的情况。因此,具有 U 锁的事务可以升级到 X 安全,因为这样做不会与另一个事务发生死锁。

所有这些与问题中给出的场景的相关性是,一个线程的事务使用 U 锁在搜索可用行时锁定行,从而阻止所有其他线程的事务。这是因为当一个事务试图在一个已经有不兼容锁的行上获取锁时,它只是在队列中等待,直到阻塞锁被解锁。因为所有线程都在相同的行中搜索空闲的行,所以它们都试图在同一行上获得 U 锁,并且都形成一个有序的队列,等待在同一行上获得 U 锁。换句话说一次只允许一个线程的事务查找空闲行。

READPAST table 提示的作用是停止排队读取 table 中的行的事务。使用READPAST,当事务厌倦了在已经锁定的行上获取锁而不是加入队列以获得锁时,它会说填充这个然后去尝试下一行。在这种情况下,它会说我不知道​​该行是否有 ClaimedBy 值,我不准备等待发现,所以我假设它有并尝试下一行。这可能意味着它会跳过可用的行,但不会得到不可用的行。这将提高线程及其事务从队列中获取项目的速度,因为它们可以同时查找可用行。

获取锁可能会非常昂贵。这需要时间和记忆。为了解决这个问题 SQL 服务器有几种锁定粒度。您可以锁定整个数据库,整个 table、一页 table 或一行 table。查询优化器将尝试使用统计信息来预测需要锁定多少行。如果有很多,它会选择页锁或 table 锁而不是行锁。这具有整体上需要更少锁的效果。

ROWLOCK table 提示告诉 SQL 服务器不要使用这些粗粒度的锁,而只使用行锁。这在这种情况下是有利的,因为它阻止了正在寻找可用行的事务跳过大量可用行。

另一种选择可能是使用 UPDATE 语句更新 table 将声明的 ID 捕获到一个变量中,两者都在同一个单一声明。

此示例将从声明的行中捕获 CatQueueItem.ID 到变量 @ClaimedID,并在一个原子操作中更新 CatQueueItem.ClaimedBy

DECLARE @ClaimedId INT

UPDATE TOP (1) q SET
       @ClaimedId = q.ID,
       ClaimedBy = 'Mittens'
  FROM CatQueueItem q
 WHERE ClaimedBy IS NULL
  1. TOP (1) 确保我们只取一件物品
  2. @ClaimedId = q.ID 获取我们即将领取的物品的 ID
  3. ClaimedBy = 'Mittens' 将项目标记为已声明
  4. WHERE ClaimedBy IS NULL 将我们限制为仅无人认领的物品

如果没有可领取的物品,@ClaimedId 将为 NULL。

如果你想确保项目按照插入的顺序被声明(一个真正的队列),那么你应该在 ID 上添加一个聚集索引 - 尽管队列不需要索引来简单地是原子的。