在SQL中使用连接进行原子更新

时间:2017-02-26 09:19:37

标签: sql sql-server join atomic database-update

我有一张桌子跟踪"签出"对象,但对象存在于各种其他表中。目标是允许用户签出符合其标准的对象,使得给定对象只能被签出一次(即,没有两个用户应该能够签出相同的对象)。在某些情况下,单个对象可能跨越多个表,需要使用连接来检查所有用户的标准(如果重要)。

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

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

1 个答案:

答案 0 :(得分:0)

使用事务和一些表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的行上放置了更新锁定。其他事务将无法更新或删除该行,但允许他们选择它,但是并发选择尝试在此行上获取更新锁(即,具有相同搜索条件的差异进程的此过程的另一次运行)将无法选择此特定行,但它可以选择并锁定下一行,因为我们也在使用readpast

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

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

参考文献:

使用公用表表达式替换一步代码:

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;