我正在执行许多并发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所示,但此尝试仍存在同样的问题。Upsert
语句实现我的MERGE
方法,但这也遇到了同样的问题。我的问题
UNIQUE
键约束冲突吗?UPDLOCK
表提示(或任何其他表提示),我该如何将其添加到我的代码中?我会将它添加到INSERT
吗? SELECT
?既?答案 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的评论并且这是一个很好的解决方法,我应该说我提出的解决方案是基于这个假设,你不想使用缓冲区或队列。