【问题标题】:How to avoid "Violation of UNIQUE KEY constraint" when doing LOTS of concurrent INSERTs执行大量并发 INSERT 时如何避免“违反 UNIQUE KEY 约束”
【发布时间】:2013-12-06 19:59:24
【问题描述】:

我正在执行许多并发 SQL INSERT 语句,这些语句在 UNIQUE KEY 约束上发生冲突,即使我还在单个事务中检查给定键的现有记录。我正在寻找一种方法来消除或最小化我遇到的碰撞数量,而不会影响性能(太多)。

背景:

我正在开发一个 ASP.NET MVC4 WebApi 项目,该项目接收大量对 INSERT 记录的 HTTP POST 请求。它每秒收到大约 5K - 10K 个请求。该项目的唯一责任是重复数据删除和汇总记录。写得很重;它的读取请求量相对较少;所有这些都使用IsolationLevel.ReadUncommitted的事务。

数据库架构

这是数据库表:

CREATE TABLE [MySchema].[Records] ( 
    Id BIGINT IDENTITY NOT NULL, 
    RecordType TINYINT NOT NULL, 
    UserID BIGINT NOT NULL, 
    OtherID SMALLINT NULL, 
    TimestampUtc DATETIMEOFFSET NOT NULL, 
    CONSTRAINT [UQ_MySchemaRecords_UserIdRecordTypeOtherId] UNIQUE CLUSTERED ( 
        [UserID], [RecordType], [OtherID] 
    ), 
    CONSTRAINT [PK_MySchemaRecords_Id] PRIMARY KEY NONCLUSTERED ( 
        [Id] ASC 
    ) 
) 

存储库代码

这是导致异常的Upsert 方法的代码:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using Dapper;

namespace MyProject.DataAccess
{
    public class MyRepo
    {
        public void Upsert(MyRecord record)
        {
            var dbConnectionString = "MyDbConnectionString";
            using (var connection = new SqlConnection(dbConnectionString))
            {
                connection.Open();
                using (var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted))
                {
                    try
                    {
                        var existingRecord = FindByByUniqueKey(transaction, record.RecordType, record.UserID, record.OtherID);

                        if (existingRecord == null)
                        {
                            const string sql = @"INSERT INTO [MySchema].[Records] 
                                                 ([UserID], [RecordType], [OtherID], [TimestampUtc]) 
                                                 VALUES (@UserID, @RecordType, @OtherID, @TimestampUtc) 
                                                 SELECT CAST(SCOPE_IDENTITY() AS BIGINT";
                            var results = transaction.Connection.Query<long>(sql, record, transaction);
                            record.Id = results.Single();
                        }
                        else if (existingRecord.TimestampUtc <= record.TimestampUtc)
                        {
                            // UPDATE
                        }

                        transaction.Commit();
                    }
                    catch (Exception e)
                    {
                        transaction.Rollback();
                        throw e;
                    }
                }
            }
        }

        // all read-only methods use explicit transactions with IsolationLevel.ReadUncommitted

        private static MyRecord FindByByUniqueKey(SqlTransaction transaction, RecordType recordType, long userID, short? otherID)
        {
            const string sql = @"SELECT * from [MySchema].[Records] 
                                 WHERE [UserID] = @UserID
                                 AND [RecordType] = @RecordType
                                 AND [OtherID] = @OtherID";
            var paramz = new {
                UserID = userID,
                RecordType = recordType,
                OtherID = otherID
            };
            var results = transaction.Connection.Query<MyRecord>(sql, paramz, transaction);
            return results.SingleOrDefault();
        }
    }

    public class MyRecord
    {
        public long ID { get; set; }
        public RecordType RecordType { get; set; }
        public long UserID { get; set; }
        public short? OtherID { get; set; }
        public DateTimeOffset TimestampUtc { get; set; }
    }

    public enum RecordType : byte
    {
        TypeOne = 1,
        TypeTwo = 2,
        TypeThree = 3
    }
}

问题

当服务器负载足够重时,我看到许多这样的异常发生:

违反 UNIQUE KEY 约束“UQ_MySchemaRecords_UserIdRecordTypeOtherId”。无法在对象“MySchema.Records”中插入重复键。重复键值为 (1234567890, 1, 123)。该语句已终止。

此异常经常发生,每分钟多达 10 次。

我的尝试

  • 我尝试将IsolationLevel 更改为Serializable。异常发生的频率要低得多,但仍然会发生。此外,代码的性能也受到很大影响;系统每秒只能处理 2K 个请求。我怀疑吞吐量的下降实际上是异常减少的原因,所以我得出结论,这并没有解决我的问题。
  • 我曾考虑使用UPDLOCK Table Hint,但我不完全了解它如何与隔离级别配合或如何将其应用到我的代码中。不过,根据我目前的理解,这似乎是最好的解决方案。
  • 我还尝试将初始 SELECT 语句(用于现有记录)添加到 INSERT 语句的一部分,如 here 所示,但此尝试仍然遇到同样的问题。
  • 我尝试使用 SQL MERGE 语句来实现我的 Upsert 方法,但这也遇到了同样的问题。

我的问题

  • 我能做些什么来防止这种类型的UNIQUE 键约束冲突吗?
  • 如果我应该使用UPDLOCK 表提示(或任何其他表提示),我将如何将它添加到我的代码中?我会将它添加到INSERT 吗? SELECT?两者都有?

【问题讨论】:

  • 如果两个人尝试记录“相同”的行,你只想赢一个吗?如果没有立即插入行有关系吗?我可以设想使用一个队列表和一个后台作业,在大量插入之前消除任何欺骗。您遇到的最大问题是您尝试使用单独的插入语句以及所有连接和 .net 事务脚手架以及随之而来的开销每秒执行 10K 单独的插入。也许应用程序应该保留一组值,例如使用表值参数,而不是执行单例插入...
  • @AaronBertrand 如果两个人尝试INSERT“相同”行,我预计最后只存在 1 条记录。如果稍后出现更新的记录,我预计会出现UPDATE;表中实际上有更多列,如果 TimestampUtc 值比现有记录更新,我会更新这些列。当前的行为现在实际上是正确的,但我得到的异常数量似乎不太理想。我希望找到一种方法来获得与现在相同的最终结果,而不会出现太多错误。
  • 根据此处的 cmets,我建议您可能还有一个不会引发异常的附加问题:在不应该对记录执行更新的情况下,因为另一条记录与更高的时间戳已用于更新。如果您还没有这样做,您可以通过将时间戳条件添加到更新的 where 子句来管理它。
  • @Carth 我从来没想过。在执行UPDATE 之前,我确实比较了代码中的TimestampUtc 值,但我的SQL 不包括该检查条件。我很可能会用旧数据覆盖新数据。接得好!我现在就解决这个问题...

标签: c# sql sql-server database dapper


【解决方案1】:

使验证读取锁定:

FROM SomeTable WITH (UPDLOCK, ROWLOCK, HOLDLOCK)

这会序列化对单个键的访问,从而允许所有其他键的并发。


HOLDLOCK ( = SERIALIZABLE) 保护一系列值。这可确保不存在的行继续不存在,因此 INSERT 成功。

UPDLOCK 确保任何现有行不会被另一个并发事务更改或删除,因此UPDATE 成功。

ROWLOCK 鼓励引擎采用行级锁。

这些更改可能会增加死锁的可能性。

【讨论】:

  • 我认为这应该添加到SELECT 语句中?另外,您说这会序列化对单个键的访问……那必须是我的 PK 还是我的 UNIQUE 键也可以工作?我一定会试试这个...
  • 是的选择。将在正在读取的任何行上获取锁。在实践中,这将是 CI 行和用于查找它的索引行。
【解决方案2】:

在你的场景中允许和抑制错误可能比试图消除它们更快。如果您使用重叠数据同步合并多个源,则需要在某处创建瓶颈来管理竞争条件。

您可以创建一个单例管理器类,该类在哈希集中保存记录的唯一约束,以便在将重复项添加到集合时自动删除它们。记录在提交到数据库之前添加,并在语句完成后删除。这样,要么哈希集吃掉重复项,要么您在尝试顶部执行的现有记录检查检测到已提交的重复记录。

【讨论】:

  • 感谢您的意见! “允许和抑制错误可能会更快”这是我目前的印象;我尝试的所有其他事情似乎都会减慢 everything 的速度,因为我当前的代码每分钟仅在 10 条记录上失败,而每秒处理 5K 条记录。不幸的是,“单例管理器类”(或任何内存中)解决方案不是一个选项,此代码在服务器集群上运行。如果我想做这样的事情,我可能不得不像@AaronBertrand 建议的那样实现某种外部队列。
  • @JesseWebb 绝对不是最优雅的解决方案,但在一分钟内 10 次我可能会建议像你现在正在做的那样捕捉异常,然后将其重新提交到 Upsert...一次
  • 这实际上也是我的 DBA 建议的:吃掉异常(因为它是预期的),重试一次,如果仍然失败则抛出。我正在避开这条路线,但我可能不得不试一试......
【解决方案3】:

AFAIK,唯一的解决方案是在insert 之前检查重复。它要求至少往返一次 DB 会导致性能不佳。

您可以在表上执行SELECT 并保持锁定以防止其他并行线程到SELECT 并获得相同的值。以下是详细解决方案:Pessimistic locking in EF code first

附言: 根据 Aron 的评论和很好的解决方法,我应该说我提出的解决方案是基于您不想使用缓冲区或队列的假设。

【讨论】:

  • 我的代码在执行INSERT 之前已经执行了SELECT 来检查现有记录。我没有使用 EntityFramework,我使用的是 dapper。有没有办法用 Dapper 或直接 SQL 进行“悲观锁定”?
  • 无论您使用什么,EF、Dapper、nativity SQL,甚至是存储过程,您都可以对表执行 SELECT 并持有锁。我猜你的问题是,在检查重复和插入时间之间,另一条记录进入并更快地插入,从而导致竞争。解决方案:锁定!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-01-17
  • 1970-01-01
  • 1970-01-01
  • 2011-04-16
  • 1970-01-01
  • 2023-03-30
  • 1970-01-01
相关资源
最近更新 更多