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

时间:2013-12-06 19:59:24

标签: c# sql sql-server database dapper

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

背景

我正在开发一个ASP.NET MVC4 WebApi项目,该项目向POST条记录接收大量HTTP INSERT请求。它每秒大约需要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请求。我怀疑吞吐量的减少实际上是减少Exceptions的原因所以我得出结论,这并没有解决我的问题。
  • 我考虑使用UPDLOCK Table Hint,但我不完全了解它如何与隔离级别合作或如何将其应用于我的代码。从我目前的理解来看,它似乎可能是最好的解决方案。
  • 我还尝试将初始SELECT语句(对于现有记录)添加为INSERT语句的一部分,如here所示,但此尝试仍存在同样的问题。
  • 我尝试使用SQL Upsert语句实现我的MERGE方法,但这也遇到了同样的问题。

我的问题

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

3 个答案:

答案 0 :(得分:3)

使验证读取锁定:

FROM SomeTable WITH (UPDLOCK, ROWLOCK, HOLDLOCK)

这序列化了对单个密钥的访问,允许所有其他密钥的并发。


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

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

ROWLOCK 鼓励引擎进行行级锁定。

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

答案 1 :(得分:1)

允许和抑制场景中的错误比尝试消除错误更快。如果您要与重叠数据同步整合多个源,则需要在某处创建瓶颈以管理竞争条件。

您可以创建一个单独的管理器类,该类在哈希集中保存记录的唯一约束,因此在将重复项添加到集合时会自动删除重复项。记录在提交到数据库之前添加,并在语句完成时删除。这样,无论是hashset还是重复,你在try的顶部做的现有记录检查都会检测到已提交的重复记录。

答案 2 :(得分:0)

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

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

<强> PS : 基于Aron的评论并且这是一个很好的解决方法,我应该说我提出的解决方案是基于这个假设,你不想使用缓冲区或队列。