【发布时间】:2013-12-06 19:59:24
【问题描述】:
我正在执行许多并发 SQL INSERT 语句,这些语句在 UNIQUE KEY 约束上发生冲突,即使我还在单个事务中检查给定键的现有记录。我正在寻找一种方法来消除或最小化我遇到的碰撞数量,而不会影响性能(太多)。
背景:
我正在开发一个 ASP.NET MVC4 WebApi 项目,该项目接收大量对 INSERT 记录的 HTTP POST 请求。它每秒收到大约 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 个请求。我怀疑吞吐量的下降实际上是异常减少的原因,所以我得出结论,这并没有解决我的问题。 - 我曾考虑使用
UPDLOCKTable Hint,但我不完全了解它如何与隔离级别配合或如何将其应用到我的代码中。不过,根据我目前的理解,这似乎是最好的解决方案。 - 我还尝试将初始
SELECT语句(用于现有记录)添加到INSERT语句的一部分,如 here 所示,但此尝试仍然遇到同样的问题。 - 我尝试使用 SQL
MERGE语句来实现我的Upsert方法,但这也遇到了同样的问题。
我的问题
- 我能做些什么来防止这种类型的
UNIQUE键约束冲突吗? - 如果我应该使用
UPDLOCK表提示(或任何其他表提示),我将如何将它添加到我的代码中?我会将它添加到INSERT吗?SELECT?两者都有?
【问题讨论】:
-
如果两个人尝试记录“相同”的行,你只想赢一个吗?如果没有立即插入行有关系吗?我可以设想使用一个队列表和一个后台作业,在大量插入之前消除任何欺骗。您遇到的最大问题是您尝试使用单独的插入语句以及所有连接和 .net 事务脚手架以及随之而来的开销每秒执行 10K 单独的插入。也许应用程序应该保留一组值,例如使用表值参数,而不是执行单例插入...
-
@AaronBertrand 如果两个人尝试
INSERT“相同”行,我预计最后只存在 1 条记录。如果稍后出现更新的记录,我预计会出现UPDATE;表中实际上有更多列,如果TimestampUtc值比现有记录更新,我会更新这些列。当前的行为现在实际上是正确的,但我得到的异常数量似乎不太理想。我希望找到一种方法来获得与现在相同的最终结果,而不会出现太多错误。 -
根据此处的 cmets,我建议您可能还有一个不会引发异常的附加问题:在不应该对记录执行更新的情况下,因为另一条记录与更高的时间戳已用于更新。如果您还没有这样做,您可以通过将时间戳条件添加到更新的 where 子句来管理它。
-
@Carth 我从来没想过。在执行
UPDATE之前,我确实比较了代码中的TimestampUtc值,但我的SQL 不包括该检查条件。我很可能会用旧数据覆盖新数据。接得好!我现在就解决这个问题...
标签: c# sql sql-server database dapper