Azure Sql Server无法使用检查约束和触发器保持数据一致性

Azure Sql Server无法使用检查约束和触发器保持数据一致性,sql,sql-server,entity-framework,azure-sql-database,transaction-isolation,Sql,Sql Server,Entity Framework,Azure Sql Database,Transaction Isolation,我试图保持Azure Sql Server数据库中的数据一致性,并实现了两种方案: 检查约束 插入/更新触发器 它们都不起作用,当我的支票被绕过时,我仍然能够再现一种情况。规则很简单-一个用户不能有多个活动分配 Tasks: - Id - UserId - Status ('Active', 'Done') User - Id - ... 进近#1-检查约束 我实现了一个确保数据一致性的函数,并将其作为检查约束应用 create function [dbo].[fnCheckUse

我试图保持Azure Sql Server数据库中的数据一致性,并实现了两种方案:

  • 检查约束
  • 插入/更新触发器
  • 它们都不起作用,当我的支票被绕过时,我仍然能够再现一种情况。规则很简单-
    一个用户不能有多个活动分配

    Tasks:
     - Id
     - UserId
     - Status ('Active', 'Done')
    
    User
     - Id
     - ...
    
    进近#1-
    检查约束

    我实现了一个确保数据一致性的函数,并将其作为检查约束应用

    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-
    触发

    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),那么其中一些分配将被约束/触发器拒绝,另一些分配将能够同时在数据库中保存UserId。因此,由数据库生成的数据将不一致

    我已经在ManagementStudio中验证了这两种方法,它可以防止我破坏数据。我无法将多个“活动”任务分配给给定用户

    重要的是,我在后端使用
    实体框架6.x
    来保存数据(
    savechangesync
    ),每个保存操作都在一个单独的事务中执行,具有默认事务隔离级别
    readcommitted


    我的方法可能有什么错误,以及如何保持数据的一致性?

    这是一个典型的比赛条件

    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
    函数,它们将从
    任务中获得相同的
    计数。然后每个线程将继续插入行,数据库的最终状态将违反您的约束

    如果您想使用
    检查约束中的函数或触发器保持您的方法,您应该确保您的事务隔离级别设置为
    可序列化
    。或者使用查询提示锁定表。或者使用
    sp_getapplock
    序列化对函数/触发器的调用


    在您的例子中,检查非常简单,因此它可以在没有触发器或函数的情况下实现。我会使用:

    此唯一索引将保证没有两个状态为“活动”的行具有相同的用户ID



    dba.se上有一个类似的问题,有更详细的解释。他们提到了另一种可能的解决方案-索引视图,它可以再次归结为唯一索引。

    这是可行的,我感到惊讶的是,检查约束也受到竞争条件的约束。对我来说,这是一个独特的索引。在我的情况下,我没有义务使用函数或触发器,我的条件就是这么简单,所以我使用唯一的过滤索引,它可以工作。现在我的代码中仍然存在异常,但只要数据始终保持一致,就可以优雅地处理它们。谢谢实际上,函数的检查约束变成了由两个(或更多步骤)组成的非原子操作。您的函数在与触发它的语句(
    INSERT
    UPDATE
    )相同的事务隔离级别下运行。它周围的引擎没有“魔法”可以阻止比赛状态。当涉及到唯一(过滤)索引时,引擎具有“魔力”,确保索引即使在高并发负载下也保持唯一。如果您想进一步研究此主题,我建议搜索
    检查约束udf
    。例如:Azure SQL数据库似乎将我的
    Read Committed
    隔离级别(我在代码中明确选择了该级别)更改为
    Read Committed Snapshot
    ,我来这里是想说“filtered unique constraint”!
    CREATE UNIQUE NONCLUSTERED INDEX [IX_UserID] ON [dbo].[Tasks]
    (
        [UserID] ASC
    ) 
    WHERE (Status = 'Active')