我有一张桌子跟踪"签出"对象,但对象存在于各种其他表中。目标是允许用户签出符合其标准的对象,使得给定对象只能被签出一次(即,没有两个用户应该能够签出相同的对象)。在某些情况下,单个对象可能跨越多个表,需要使用连接来检查所有用户的标准(如果重要)。
这是一个非常简单的示例查询(希望您可以推断出架构):
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
答案 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
updlock
在select
的行上放置了更新锁定。其他事务将无法更新或删除该行,但允许他们选择它,但是并发选择尝试在此行上获取更新锁(即,具有相同搜索条件的差异进程的此过程的另一次运行)将无法选择此特定行,但它可以选择并锁定下一行,因为我们也在使用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;