Azure Sql 服务器无法使用检查约束和触发器保持数据一致性
Azure Sql Server fails to keep data consistency using a check constraint and a trigger
我试图在我的 Azure Sql 服务器数据库中保持数据一致性并实施了两种方案:
- 检查约束
- insert/update 触发器
None 他们的工作,我仍然能够重现我的检查被绕过的情况。规则很简单 - there couldn't be more than one active assignment for a user
.
Tasks:
- Id
- UserId
- Status ('Active', 'Done')
User
- Id
- ...
方法 #1 - Check Constraints
我已经实现了一个功能来确保数据的一致性并将其应用为检查约束
create function [dbo].[fnCheckUserConcurrentAssignment]
(
@id nvarchar(128),
@status nvarchar(50), -- not used but required to check constraint
)
returns bit
as
begin
declare @result bit
select @result = cast(case when (select count(t.Id)
from dbo.Tasks t
where t.[UserId] = @id
and t.[Status != 'Done') > 1
then 1
else 0
end as bit)
return @result
end
alter table dbo.Tasks
with check add constraint [CK_CheckUserConcurrentAssignment]
check (dbo.fnCheckUserConcurrentAssignment([UserId], [Status]) = 0)
方法 #2 - Trigger
alter trigger dbo.[TR_CheckUserConcurrentAssignment]
on dbo.Tasks
for insert, update
as begin
if(exists(select conflictId from
(select (select top 1 t.Id from dbo.Tasks t
where t.UserId = i.UserId
and o.[Status] != N'Done'
and o.Id != i.Id) as conflictId
from inserted i
where i.UserId is not null) conflicts
where conflictId is not null))
begin
raiserror ('Concurrent user assignment detected.', 16, 1);
rollback;
end
end
如果我并行创建大量分配(通常 > 10),那么其中一些将被 constraint/trigger 拒绝,其他将能够同时将 UserId 保存在数据库中。这样一来数据库数据就会不一致。
我已经在 Management Studio 中验证了这两种方法,它可以防止我破坏数据。我无法将多个 'Active' 任务分配给给定用户。
重要的是,我在后端使用 Entity Framework 6.x
来保存我的数据 (SaveChangesAsync
),并且每个保存操作都在具有默认事务隔离级别的单独事务中执行 ReadCommited
我的方法可能有什么问题以及如何使我的数据保持一致?
这是一个经典的竞争条件。
select @result = cast(case when (select count(t.Id)
from dbo.Tasks t
where t.[UserId] = @id
and t.[Status != 'Done') > 1
then 1
else 0
end as bit)
如果两个线程运行同时运行fnCheckUserConcurrentAssignment
,它们将从Tasks
得到相同的count
。然后每个线程将继续插入行,数据库的最终状态将违反您的约束。
如果您想使用 CHECK
约束或触发器中的函数,您应该确保您的事务隔离级别设置为 SERIALIZABLE
。或者使用查询提示锁定table。或者使用 sp_getapplock
序列化对 function/trigger.
的调用
对于您的情况,检查非常简单,因此无需触发器或函数即可实现。我会使用 filtered unique index:
CREATE UNIQUE NONCLUSTERED INDEX [IX_UserID] ON [dbo].[Tasks]
(
[UserID] ASC
)
WHERE (Status = 'Active')
这个唯一索引将保证没有两行 Status = 'Active'
具有相同的 UserID
.
dba.se How are my SQL Server constraints being bypassed? 上有类似的问题,有更详细的解释。他们提到了另一种可能的解决方案——索引视图,它再次归结为唯一索引。
我试图在我的 Azure Sql 服务器数据库中保持数据一致性并实施了两种方案:
- 检查约束
- insert/update 触发器
None 他们的工作,我仍然能够重现我的检查被绕过的情况。规则很简单 - there couldn't be more than one active assignment for a user
.
Tasks:
- Id
- UserId
- Status ('Active', 'Done')
User
- Id
- ...
方法 #1 - Check Constraints
我已经实现了一个功能来确保数据的一致性并将其应用为检查约束
create function [dbo].[fnCheckUserConcurrentAssignment]
(
@id nvarchar(128),
@status nvarchar(50), -- not used but required to check constraint
)
returns bit
as
begin
declare @result bit
select @result = cast(case when (select count(t.Id)
from dbo.Tasks t
where t.[UserId] = @id
and t.[Status != 'Done') > 1
then 1
else 0
end as bit)
return @result
end
alter table dbo.Tasks
with check add constraint [CK_CheckUserConcurrentAssignment]
check (dbo.fnCheckUserConcurrentAssignment([UserId], [Status]) = 0)
方法 #2 - Trigger
alter trigger dbo.[TR_CheckUserConcurrentAssignment]
on dbo.Tasks
for insert, update
as begin
if(exists(select conflictId from
(select (select top 1 t.Id from dbo.Tasks t
where t.UserId = i.UserId
and o.[Status] != N'Done'
and o.Id != i.Id) as conflictId
from inserted i
where i.UserId is not null) conflicts
where conflictId is not null))
begin
raiserror ('Concurrent user assignment detected.', 16, 1);
rollback;
end
end
如果我并行创建大量分配(通常 > 10),那么其中一些将被 constraint/trigger 拒绝,其他将能够同时将 UserId 保存在数据库中。这样一来数据库数据就会不一致。
我已经在 Management Studio 中验证了这两种方法,它可以防止我破坏数据。我无法将多个 'Active' 任务分配给给定用户。
重要的是,我在后端使用 Entity Framework 6.x
来保存我的数据 (SaveChangesAsync
),并且每个保存操作都在具有默认事务隔离级别的单独事务中执行 ReadCommited
我的方法可能有什么问题以及如何使我的数据保持一致?
这是一个经典的竞争条件。
select @result = cast(case when (select count(t.Id)
from dbo.Tasks t
where t.[UserId] = @id
and t.[Status != 'Done') > 1
then 1
else 0
end as bit)
如果两个线程运行同时运行fnCheckUserConcurrentAssignment
,它们将从Tasks
得到相同的count
。然后每个线程将继续插入行,数据库的最终状态将违反您的约束。
如果您想使用 CHECK
约束或触发器中的函数,您应该确保您的事务隔离级别设置为 SERIALIZABLE
。或者使用查询提示锁定table。或者使用 sp_getapplock
序列化对 function/trigger.
对于您的情况,检查非常简单,因此无需触发器或函数即可实现。我会使用 filtered unique index:
CREATE UNIQUE NONCLUSTERED INDEX [IX_UserID] ON [dbo].[Tasks]
(
[UserID] ASC
)
WHERE (Status = 'Active')
这个唯一索引将保证没有两行 Status = 'Active'
具有相同的 UserID
.
dba.se How are my SQL Server constraints being bypassed? 上有类似的问题,有更详细的解释。他们提到了另一种可能的解决方案——索引视图,它再次归结为唯一索引。