Я выполняю множество параллельных операторов SQL INSERT
, которые сталкиваются с ограничением UNIQUE KEY, хотя я также проверяю существующие записи для данного ключа внутри одной транзакции. Я ищу способ устранить или свести к минимуму количество коллизий, которые я получаю без ущерба для производительности (слишком много).
Фон:
Я работаю над проектом ASP.NET MVC4 WebApi, который получает МНОГО HTTP POST
запросов к INSERT
записям. Он получает около 5K - 10K запросов в секунду. Ответственность за проект несет де-дублирование и агрегирование записей. Это очень тяжело писать; он имеет относительно небольшое количество запросов на чтение; все из которых используют транзакцию с 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
)
)
Код репозитория
Вот код для метода 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
, как показано здесь, но эта попытка все еще была та же проблема. - Я попробовал реализовать мой метод
Upsert
с помощью инструкции SQLMERGE
, но это также пострадало от той же проблемы.
Мой вопрос (ы)
- Есть ли что-нибудь, что я могу сделать, чтобы предотвратить столкновение с ограничениями типа
UNIQUE
? - Если я должен использовать подсказку таблицы
UPDLOCK
(или любую другую подсказку в таблице), как бы добавить это к моему коду? Добавлю ли я его вINSERT
?SELECT
? Оба?