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次.
我试过了什么
IsolationLevel
到Serializable
.异常发生得少得多,但仍然发生.而且,代码的性能受到很大影响; 系统每秒只能处理2K请求.我怀疑吞吐量的减少实际上是减少Exceptions的原因所以我得出结论,这并没有解决我的问题.UPDLOCK
表提示,但我不完全理解它如何与隔离级别合作或如何将其应用于我的代码.从我目前的理解来看,它似乎可能是最好的解决方案.SELECT
语句(对于现有记录)添加为语句的一部分INSERT
,如此处所示,但此尝试仍然存在相同的问题.Upsert
使用SQL MERGE
语句实现我的方法,但这也遇到了同样的问题.我的问题
UNIQUE
键约束碰撞吗?UPDLOCK
表提示(或任何其他表提示),我将如何将其添加到我的代码中?我会把它添加到INSERT
?的SELECT
?都?使验证读取锁定:
FROM SomeTable WITH (UPDLOCK, ROWLOCK, HOLDLOCK)
Run Code Online (Sandbox Code Playgroud)
这会序列化对单个键的访问,从而允许对所有其他键进行并发操作。
HOLDLOCK
( = SERIALIZABLE
) 保护一系列值。这确保不存在的行继续不存在,因此成功INSERT
。
UPDLOCK
确保任何现有行不会被另一个并发事务更改或删除,以便UPDATE
成功。
ROWLOCK
鼓励引擎采用行级锁。
这些变化可能会增加陷入僵局的可能性。