在 SQL 中加入原子更新

Atomic update with joins in SQL

我有一个 table 可以跟踪 "checked out" 对象,但对象存在于其他各种 table 中。目标是允许用户签出符合他们条件的对象,这样一个给定的对象只能签出一次(即没有两个用户应该能够签出同一个对象)。在某些情况下,单个对象可能跨越多个 table,需要使用连接来检查所有用户的条件(以防万一)。

这是一个非常简单的示例查询(希望您能推断出架构):

update top (1) Tracker
set IsCheckedOut = 1
  from Tracker t
  join Object o on t.ObjectId = o.Id
  join Property p on p.ObjectId = o.Id
  where t.IsCheckedOut = 0
  and o.SomePropertyColumn = 'blah'
  and p.SomeOtherPropertyColumn = 42

由于 from 子查询,我怀疑这个查询不是原子的,因此两个用户同时请求相同风格的对象最终可能会检出同一个对象。

这是真的吗?如果是这样,我该如何解决?

我想过添加一个 output DELETED.* 子句,如果 IsCheckedOut 列的返回值是 1,我想让用户重试他们的查询,我认为这可行(如果我是,请纠正我错误)...但我想得到一些用户不必担心重试的东西。

编辑

有关详尽的解释,请参阅下面的 SqlZim 答案,但对于这个简单的案例,我可以直接将提示添加到上面发布的查询中:

update top (1) Tracker
set IsCheckedOut = 1
  from Tracker t (updlock, rowlock, readpast)
  join Object o on t.ObjectId = o.Id
  join Property p on p.ObjectId = o.Id
  where t.IsCheckedOut = 0
  and o.SomePropertyColumn = 'blah'
  and p.SomeOtherPropertyColumn = 42

使用一个事务和一些 table hints 进行锁定,我们可以只抓取一行并保留它以进行更新。

declare @TrackerId int;

begin tran;

select top 1 @TrackerId = TrackerId
  from Tracker t with (updlock, rowlock, readpast)
    inner join Object o on t.ObjectId = o.Id
    inner join Property p on p.ObjectId = o.Id;
  where t.IsCheckedOut = 0
    and o.SomePropertyColumn = 'blah'
    and p.SomeOtherPropertyColumn = 42;

if @TrackerId is not null 
begin;
update Tracker
  set IsCheckedOut = 1
  where TrackerId = @TrackerId;
end;

commit tran
  • updlockselect 的行上放置更新锁。其他事务将无法更新或删除该行,但允许它们 select 它,但是并发 select 试图获取该行的更新锁(即另一个 运行此过程来自具有相同搜索条件的不同进程)将无法 select 此特定行,但是它可以 select 并锁定下一行,因为我们也在使用 readpast

  • rowlock 尝试只锁定我们要更新的特定行,而不是页面或 table 锁。

  • readpast 跳过具有行级锁的行。

参考文献:


使用通用 table 表达式交替一步代码:

begin tran;

  with cte as (
    select top 1 
        t.*
      from Tracker t with (updlock, rowlock, readpast)
        inner join Object o 
          on t.ObjectId = o.Id
        inner join Property p 
          on p.ObjectId = o.Id;
      where t.IsCheckedOut = 0
        and o.SomePropertyColumn = 'blah'
        and p.SomeOtherPropertyColumn = 42
      --order by TrackerId asc /* optional order by */
  )
  update cte
    set IsCheckedOut = 1
    output inserted.*;

commit tran;