【问题标题】:EF Core - Concurrent saving of the same entity creates multipleEF Core - 同一实体的并发保存会创建多个
【发布时间】:2020-09-12 14:07:31
【问题描述】:

问题
我的概念是,当用户创建带有一些标签的帖子时,服务器首先检查标签名称是否已经存在,如果存在,它的计数器会增加,否则会创建一个新标签。

当多个用户同时创建一个带有新标签的帖子时,问题就出现了,比如说new_tag,然后多个同名标签会保留在数据库中,而不是 1 个标签,计数器 = # of users who used this标记

如您所见,每个用户都会在数据库中创建一个新的标签记录:

--------------------------------
|  id  |  tagName  |  counter  |
|------|-----------|-----------|
|   1  |  new_tag  |    1      |
|   2  |  new_tag  |    1      |
|   3  |  new_tag  |    1      |
|   4  |  new_tag  |    1      |
--------------------------------

我的期望:

--------------------------------
|  id  |  tagName  |  counter  |
|------|-----------|-----------|
|   1  |  new_tag  |    4      |
--------------------------------

这段代码展示了我是如何实现持久化的:


PostRepository

public async Task<bool> AddAsync(Post entity)
        {
            await AddNewTagsAsync(entity);
            _context.Attach(entity.Event);
            await _context.AddAsync(entity);
            await _context.Database.BeginTransactionAsync();
                var result = await _context.SaveChangesAsync();
            _context.Database.CommitTransaction();
                return result > 0;
        }

 public async Task AddNewTagsAsync(Post post)
        {
            // store tags name in lower case
            if ((post.PostTags == null) || (post.PostTags.Count==0))
                return;
            post.PostTags.ForEach(pt => pt.Tag.TagName = pt.Tag.TagName.ToLower());

            for(var i =0; i<post.PostTags.Count; i++)
            {
                var postTag = post.PostTags[i];

                // here lays the main problem, when many concurrent users check for tag existence 
                // all get null and new tag will be created, workaround needed!
                var existingTag = await _context.Tags.SingleOrDefaultAsync(x => x.TagName == postTag.Tag.TagName);

                // if tag exists, increment counter
                if (existingTag != null)
                {
                    existingTag.Counter++;
                    postTag.Tag = existingTag;
                    continue;
                }

               // else the new Tag object will be peristed   
            }
        }

这是我的 ER 图:



顺便提一下,如果一个用户先创建标签,然后其他用户只是增加计数器并使用相同的标签,它会按预期工作

【问题讨论】:

  • 愚蠢的问题,但PostTags 条目的数量实际上不代表您正在寻找的计数器吗?但是,您可能想看看 EF 的锁定技术,例如Handling Concurrency Conflicts.
  • 在处理器时间中,_context.Tags.SingleOrDefaultAsync...CommitTransaction 之间存在一个时代。这种类型的冲突只能通过唯一的数据库索引和捕获异常来解决。
  • @GertArnold 问题是,没有抛出异常。所有线程都看到这个标签不存在并创建它。
  • “所有线程都看到这个标签不存在”——这就是我的意思。他们有足够的时间得出这个结论。所以是的,索引是必要的,以使其最终安全。
  • 我认为手动保持计数是不必要的,使用索引,通过 tagid 过滤的 PostTags 中的计数即使是数百万行也将花费可以忽略不计的时间。 TagName 上的唯一索引也可以避免重复。你应该只处理异常。

标签: c# postgresql entity-framework asp.net-core entity-framework-core


【解决方案1】:

您正在寻找原子 UPSERT 语句(组合的 UPDATE 或 INSERT)。

EF Core 不支持 UPSERTS。见:https://github.com/dotnet/efcore/issues/4526

但是,如果您愿意放弃更改跟踪,您可以直接进行 SQL 合并语句,如下所示:

    MERGE INTO dbo.Tags AS target  
        USING (VALUES ({TagName})) AS source (TagName)  
        ON target.TagName = source.TagName  
    WHEN MATCHED THEN  
        UPDATE SET Counter = Counter + 1  
    WHEN NOT MATCHED BY TARGET THEN  
        INSERT (TagName, Counter) VALUES (TagName, 1);

你可以这样称呼它:

public async Task AddNewTagsAsync(Post post)
{
    foreach (var tag in post.PostTags)
    {
        await _context.Database.ExececuteInterpolatedAsync($@"
            MERGE INTO dbo.Tags AS target  
                USING (VALUES ({tag.TagName})) AS source (TagName)  
                ON target.TagName = source.TagName  
            WHEN MATCHED THEN  
                UPDATE SET Counter = Counter + 1  
            WHEN NOT MATCHED BY TARGET THEN  
                INSERT (TagName, Counter) VALUES (TagName, 1)");
    } 
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多