在 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
updlock
在 select
的行上放置更新锁。其他事务将无法更新或删除该行,但允许它们 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;
我有一个 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
updlock
在select
的行上放置更新锁。其他事务将无法更新或删除该行,但允许它们 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;