在进行大量并发INSERT时如何避免"违反UNIQUE KEY约束"

Jes*_*ebb 12 c# sql database sql-server dapper

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

背景:

我正在开发一个ASP.NET MVC4 WebApi项目,它接收大量的HTTP 记录POST请求INSERT.它每秒大约需要5K - 10K的请求.该项目的唯一责任是重复记录和汇总记录.这是非常重写; 它具有相对少量的读取请求; 所有这些都使用了一个Transaction IsolationLevel.ReadUncommitted.

数据库架构

这是DB表:

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 
    ) 
) 
Run Code Online (Sandbox Code Playgroud)

存储库代码

以下是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
    }
}
Run Code Online (Sandbox Code Playgroud)

问题

当服务器负载足够大时,我发现许多异常发生:

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

此异常经常发生,一分钟内多达10次.

我试过了什么

  • 我试图改变IsolationLevelSerializable.异常发生得少得多,但仍然发生.而且,代码的性能受到很大影响; 系统每秒只能处理2K请求.我怀疑吞吐量的减少实际上是减少Exceptions的原因所以我得出结论,这并没有解决我的问题.
  • 我考虑过使用UPDLOCK 表提示,但我不完全理解它如何与隔离级别合作或如何将其应用于我的代码.从我目前的理解来看,它似乎可能是最好的解决方案.
  • 我还尝试将初始SELECT语句(对于现有记录)添加为语句的一部分INSERT,如此处所示,但此尝试仍然存在相同的问题.
  • 我尝试Upsert使用SQL MERGE语句实现我的方法,但这也遇到了同样的问题.

我的问题

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

usr*_*usr 3

使验证读取锁定:

FROM SomeTable WITH (UPDLOCK, ROWLOCK, HOLDLOCK)
Run Code Online (Sandbox Code Playgroud)

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


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

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

ROWLOCK 鼓励引擎采用行级锁。

这些变化可能会增加陷入僵局的可能性。