使用乐观并发方法处理多个生产者插入唯一"不可变"实体的有效方法是什么?

Eug*_*kal 8 c# sql-server entity-framework optimistic-concurrency sql-insert

假设一个具有多个并发生成器的系统,每个生成器都努力使用以下可通过其名称唯一标识的公共实体来持久保存一些对象图:

CREATE TABLE CommonEntityGroup(
    Id INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
    Name NVARCHAR(100) NOT NULL
);
GO

CREATE UNIQUE INDEX IX_CommonEntityGroup_Name 
    ON CommonEntityGroup(Name)
GO


CREATE TABLE CommonEntity(
    Id INT NOT NULL IDENTITY(1, 1) PRIMARY KEY,
    Name NVARCHAR(100) NOT NULL,
    CommonEntityGroupId INT NOT NULL,
    CONSTRAINT FK_CommonEntity_CommonEntityGroup FOREIGN KEY(CommonEntityGroupId) 
        REFERENCES CommonEntityGroup(Id)
);
GO

CREATE UNIQUE INDEX IX_CommonEntity_CommonEntityGroupId_Name 
    ON CommonEntity(CommonEntityGroupId, Name)
GO
Run Code Online (Sandbox Code Playgroud)

例如,生产者A保存了一些CommonEntityMeetings,而生产者B保存了CommonEntitySets.他们中的任何一个都必须坚持CommonEntity与他们的特定物品相关.

基本上,关键点是:

  • 有独立的生产者.
  • 它们同时运作.
  • 理论上(尽管可能会改变,现在还不完全正确),它们将通过相同的Web服务(ASP.Net Web API)运行,只需使用各自的端点/"资源".因此,理想的解决方案不应该依赖于此.
  • 他们努力坚持包含可能尚未存在的CommonEntity/CommonEntityGroup对象的对象的不同图形.
  • CommonEntity/CommonEntityGroup 一旦创建就是不可变的,之后永远不会被修改或删除.
  • CommonEntity/CommonEntityGroup根据它们的一些属性(Name以及相关的公共实体,如果有的话)(例如CommonEntity,唯一的CommonEntity.Name+ CommonEntityGroup.Name)是唯一的.
  • 生产者不知道/关心那些ID CommonEntities- 他们通常只是通过DTO与Names这些CommonEntities和相关信息(唯一).所以任何Common(Group)Entity必须找到/创建Name.
  • 生产者有可能尝试同时创建相同的CommonEntity/CommonEntityGroup.
  • 虽然这样的CommonEntity/CommonEntityGroup对象更可能已经存在于db中.

因此,使用Entity Framework(数据库优先,尽管可能无关紧要)作为DAL和SQL Server作为存储,什么是一种有效且可靠的方法来确保所有这些生产者能够同时成功地保持其交叉对象图?

考虑到UNIQUE INDEX已经确保不会有重复CommonEntities(Name,GroupName对是唯一的)我可以看到以下解决方案:

  1. 在构建对象图的其余部分之前,确保找到/创建了每个CommonEntity/CommonGroupEntity + SaveChanged().

在这种情况下,当SaveChanges被调用相关实体时,由于其他生产者在此刻之前创建了相同的实体,因此不存在任何索引违规.

要实现它,我会有一些

public class CommonEntityGroupRepository // sort of
{
    public CommonEntityGroupRepository(EntitiesDbContext db) ...

    // CommonEntityRepository will use this class/method internally to create parent CommonEntityGroup.
    public CommonEntityGroup FindOrCreateAndSave(String groupName)
    {
        return
            this.TryFind(groupName) ?? // db.FirstOrDefault(...)
            this.CreateAndSave(groupName);
    }

    private CommonEntityGroup CreateAndSave(String groupName)
    {
        var group = this.Db.CommonEntityGroups.Create();
        group.Name = groupName;
        this.Db.CommonGroups.Add(group)

        try
        {
            this.Db.SaveChanges();
            return group;
        }
        catch (DbUpdateException dbExc)
        {
            // Check that it was Name Index violation (perhaps make indices IGNORE_DUP_KEY)
            return this.Find(groupName); // TryFind that throws exception.
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用这种方法会有多次调用SaveChanges,每个CommonEntity都有自己的Repository,尽管它似乎是最可靠的解决方案.

  1. 如果发生索引违规,只需创建整个图并从头开始重建

有点丑陋和低效(有10个CommonEntities我们可能要重试10次),但简单,或多或少可靠.

  1. 如果发生索引违规,只需创建整个图表并替换重复的条目

不确定是否有一种简单可靠的方法来替换或多或少复杂的对象图中的重复条目,尽管可以实现特定于案例和更通用的基于反射的解决方案.

仍然,像以前的解决方案一样,它可能需要多次重试.

  1. 尝试将此逻辑移动到数据库(SP)

怀疑在存储过程中处理它会更容易.这将是刚刚在数据库端实现的相同的乐观或悲观方法.

虽然它可以提供更好的性能(在这种情况下不是问题)并将插入逻辑放在一个共同的位置.

  1. 在存储过程中使用SERIALIZABLE隔离级别/ TABLOCKX + SERIALIZABLE表提示 - 它应该可以正常工作,但我不希望仅仅锁定表格超过实际需要,因为实际竞争非常罕见.正如标题中已经提到的那样,我想找到一些乐观的并发方法.

我可能会尝试第一种解决方案,但也许有更好的替代方案或一些潜在的陷阱.

Sql*_*Zim 4

表值参数

一种选择是使用table valued parameters数据库而不是单独调用。

使用表值参数的示例过程:

create type dbo.CommonEntity_udt as table (
    CommonEntityGroupId int not null
  , Name      nvarchar(100) not null
  , primary key (CommonEntityGroupId,Name)
    );
go

create procedure dbo.CommonEntity_set (
    @CommonEntity dbo.CommonEntity_udt readonly
) as
begin;
  set nocount on;
  set xact_abort on;
  if exists (
    select 1 
      from @CommonEntity as s
        where not exists (
          select 1 
            from dbo.CommonEntity as t
            where s.Name = t.Name
              and s.CommonEntityGroupId = t.CommonEntityGroupId
            ))
    begin;
      insert dbo.CommonEntity (Name)
        select s.Name
          from @CommonEntity as s
          where not exists (
            select 1 
              from dbo.CommonEntity as t with (updlock, holdlock)
              where s.Name = t.Name
                and s.CommonEntityGroupId = t.CommonEntityGroupId
              );
    end;
end;
go
Run Code Online (Sandbox Code Playgroud)

表值参数参考:


merge除非有令人信服的论据,否则我不推荐。这种情况只看插入,所以显得有点大材小用了。

merge具有表值参数的示例版本:

create procedure dbo.CommonEntity_merge (
    @CommonEntity dbo.CommonEntity_udt readonly
) as
begin;
  set nocount on;
  set xact_abort on;
  if exists (
    select 1 
      from @CommonEntity as s
        where not exists (
          select 1 
            from dbo.CommonEntity as t
            where s.Name = t.Name
              and s.CommonEntityGroupId = t.CommonEntityGroupId
            ))
    begin;
      merge dbo.CommonEntity with (holdlock) as t
      using (select CommonEntityGroupId, Name from @CommonEntity) as s
      on (t.Name = s.Name
        and s.CommonEntityGroupId = t.CommonEntityGroupId)
      when not matched by target
        then insert (CommonEntityGroupId, Name) 
        values (s.CommonEntityGroupId, s.Name);
    end;
end;
go
Run Code Online (Sandbox Code Playgroud)

merge参考:


ignore_dup_key代码注释:

// 检查是否违反名称索引(也许使索引 IGNORE_DUP_KEY)

ignore_dup_keyserializable在幕后使用;非聚集索引可能会产生高昂的开销;即使索引是聚集的,根据重复项的数量,也可能会产生巨大的成本

这可以在存储过程中使用Sam Saffron 的 upsert(更新/插入)模式或此处显示的模式之一来处理:不同错误处理技术的性能影响 - Aaron Bertrand