SQL 只插入新行而不执行任何选择时服务器死锁
SQL Server deadlock when only inserting new rows without performing any selects
我发现我的应用程序经常出现死锁,即使它不执行 select 语句、删除语句和更新语句。它只是插入全新的数据。
TL;DR: 好像跟外键有关。如果我删除它,那么我根本不会遇到任何死锁。但由于显而易见的原因,这不是一个可以接受的table解决方案。
给定以下table结构
CREATE TABLE [dbo].[IncomingFile]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
CONSTRAINT [PK_IncomingFile] PRIMARY KEY CLUSTERED([Id])
)
GO
CREATE TABLE [dbo].[IncomingFileEvent]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
[IncomingFileId] UNIQUEIDENTIFIER NOT NULL,
CONSTRAINT [PK_IncomingFileEvent] PRIMARY KEY CLUSTERED([Id]),
CONSTRAINT [FK_IncomingFileEvent_IncomingFileId]
FOREIGN KEY ([IncomingFileId])
REFERENCES [dbo].[IncomingFile] ([Id])
)
GO
当我遇到多个插入数据的并发任务时,我总是看到死锁。 READ_COMMITTED_SNAPSHOT
在我的数据库选项中启用(即使我没有阅读)。
这是将重现该问题的代码。如果您没有遇到问题,请增加程序顶部的 NumberOfTasksPerCpu
常量。
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SqlServerDeadlockRepro
{
class Program
{
private const int NumberOfTasksPerCpu = 8; // Keep increasing this by one if you do not get a deadlock!
private const int NumberOfChildRows = 1_000;
private const string MSSqlConnectionString = "Server=DESKTOP-G05BF1U;Database=EFCoreConcurrencyTest;Trusted_Connection=True;";
private static int NumberOfConcurrentTasks;
static async Task Main(string[] args)
{
NumberOfConcurrentTasks = Environment.ProcessorCount * NumberOfTasksPerCpu;
var readySignals = new Queue<ManualResetEvent>();
var trigger = new ManualResetEvent(false);
var processingTasks = new List<Task>();
for (int index = 0; index < NumberOfConcurrentTasks; index++)
{
var readySignal = new ManualResetEvent(false);
readySignals.Enqueue(readySignal);
var task = CreateDataWithSqlCommand(trigger, readySignal);
processingTasks.Add(task);
}
Console.WriteLine("Waiting for tasks to become ready");
while (readySignals.Count > 0)
{
var readySignalBatch = new List<WaitHandle>();
for(int readySignalCount = 0; readySignals.Count > 0 && readySignalCount < 64; readySignalCount++)
{
readySignalBatch.Add(readySignals.Dequeue());
}
WaitHandle.WaitAll(readySignalBatch.ToArray());
}
Console.WriteLine("Saving data");
var sw = Stopwatch.StartNew();
trigger.Set();
await Task.WhenAll(processingTasks.ToArray());
sw.Stop();
Console.WriteLine("Finished - " + sw.ElapsedMilliseconds);
}
private static int TaskNumber = 0;
private static async Task CreateDataWithSqlCommand(ManualResetEvent trigger, ManualResetEvent readySignal)
{
await Task.Yield();
using var connection = new SqlConnection(MSSqlConnectionString);
await connection.OpenAsync().ConfigureAwait(false);
var transaction = (SqlTransaction)await connection.BeginTransactionAsync(System.Data.IsolationLevel.ReadCommitted).ConfigureAwait(false);
Console.WriteLine("Task " + Interlocked.Increment(ref TaskNumber) + $" of {NumberOfConcurrentTasks} ready ");
readySignal.Set();
trigger.WaitOne();
Guid parentId = Guid.NewGuid();
string fileCommandSql = "insert into IncomingFile (Id) values (@Id)";
using var fileCommand = new SqlCommand(fileCommandSql, connection, transaction);
fileCommand.Parameters.Add("@Id", System.Data.SqlDbType.UniqueIdentifier).Value = parentId;
await fileCommand.ExecuteNonQueryAsync().ConfigureAwait(false);
using var fileEventCommand = new SqlCommand
{
Connection = connection,
Transaction = transaction
};
var commandTextBulder = new StringBuilder("INSERT INTO [IncomingFileEvent] ([Id], [IncomingFileId]) VALUES ");
for (var i = 1; i <= NumberOfChildRows * 2; i += 2)
{
commandTextBulder.Append($"(@p{i}, @p{i + 1})");
if (i < NumberOfChildRows * 2 - 1)
commandTextBulder.Append(',');
fileEventCommand.Parameters.AddWithValue($"@p{i}", Guid.NewGuid());
fileEventCommand.Parameters.AddWithValue($"@p{i + 1}", parentId);
}
fileEventCommand.CommandText = commandTextBulder.ToString();
await fileEventCommand.ExecuteNonQueryAsync().ConfigureAwait(false);
await transaction.CommitAsync().ConfigureAwait(false);
}
}
}
更新
还尝试制作主键 NONCLUSTERED
并根据当前日期和时间添加 CLUSTERED
索引。
CREATE TABLE [dbo].[IncomingFile]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
[CreatedUtc] DateTime2 DEFAULT GETDATE(),
CONSTRAINT [PK_IncomingFile] PRIMARY KEY NONCLUSTERED([Id])
)
GO
CREATE CLUSTERED INDEX [IX_IncomingFile_CreatedUtc] on [dbo].[IncomingFile]([CreatedUtc])
GO
CREATE TABLE [dbo].[IncomingFileEvent]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
[IncomingFileId] UNIQUEIDENTIFIER NOT NULL,
[CreatedUtc] DateTime2 DEFAULT GETDATE(),
CONSTRAINT [PK_IncomingFileEvent] PRIMARY KEY NONCLUSTERED([Id]),
CONSTRAINT [FK_IncomingFileEvent_IncomingFileId]
FOREIGN KEY ([IncomingFileId])
REFERENCES [dbo].[IncomingFile] ([Id])
)
GO
CREATE CLUSTERED INDEX [IX_IncomingFileEvent_CreatedUtc] on [dbo].[IncomingFileEvent]([CreatedUtc])
GO
更新 2
我尝试了从 here 获取的顺序 guid,但没有任何区别。
更新 3
好像跟外键有关。如果我删除它,那么我根本不会遇到任何死锁。
更新 4
Sql 服务器产品组的回复和一些建议已发布在我原来的 github 问题上。
https://github.com/dotnet/efcore/issues/21899#issuecomment-683404734
死锁是由于检查参照完整性所需的执行计划造成的。将大量 (1K) 行插入相关 IncomingFileEvent
table 时,将执行 IncomingFile
table 的完整 table 扫描。扫描获取一个共享 table 锁,该锁在事务期间持有,当不同的会话各自持有刚插入的 IncomingFile
行上的独占行锁并被另一个独占会话阻塞时,会导致死锁行锁。
下面是显示此内容的执行计划:
避免死锁的一种方法是在 IncomingFileEvent
插入查询上使用 OPTION (LOOP JOIN)
查询提示:
var commandTextBulder = new StringBuilder("INSERT INTO [IncomingFileEvent] ([Id], [IncomingFileId]) VALUES ");
for (var i = 1; i <= NumberOfChildRows * 2; i += 2)
{
commandTextBulder.Append($"(@p{i}, @p{i + 1})");
if (i < NumberOfChildRows * 2 - 1)
commandTextBulder.Append(',');
fileEventCommand.Parameters.AddWithValue($"@p{i}", Guid.NewGuid());
fileEventCommand.Parameters.AddWithValue($"@p{i + 1}", parentId);
}
commandTextBulder.Append(" OPTION (LOOP JOIN);");
这是带有提示的计划:
附带说明一下,考虑将现有主键更改为以下主键。从数据建模的角度来看(识别关系)这是更正确的,并且会提高插入和选择的性能,因为相关行在物理上聚集在一起。
CONSTRAINT [PK_IncomingFileEvent] PRIMARY KEY CLUSTERED(IncomingFileId, Id)
我编写了以下扩展来解决 EF Core 的问题。
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
base.OnConfiguring(options);
options.UseLoopJoinQueries();
}
使用此代码...
public static class UseLoopJoinQueriesExtension
{
public static DbContextOptionsBuilder UseLoopJoinQueries(this DbContextOptionsBuilder builder)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));
builder.AddInterceptors(new OptionLoopJoinCommandInterceptor());
return builder;
}
}
internal class OptionLoopJoinCommandInterceptor : DbCommandInterceptor
{
public override Task<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
{
AppendOptionToSql(command);
return Task.FromResult(result);
}
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
AppendOptionToSql(command);
return result;
}
private static void AppendOptionToSql(DbCommand command)
{
const string OPTION_TEXT = " OPTION (LOOP JOIN)";
string[] commands = command.CommandText.Split(";");
for (int index = 0; index < commands.Length; index++)
{
string sql = commands[index].Trim();
if (sql.StartsWith("insert into ", StringComparison.InvariantCultureIgnoreCase)
|| sql.StartsWith("select ", StringComparison.InvariantCultureIgnoreCase)
|| sql.StartsWith("delete ", StringComparison.InvariantCultureIgnoreCase)
|| sql.StartsWith("merge ", StringComparison.InvariantCultureIgnoreCase))
{
commands[index] += OPTION_TEXT;
}
}
#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
command.CommandText = string.Join(";\r\n", commands);
#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
}
}
我发现我的应用程序经常出现死锁,即使它不执行 select 语句、删除语句和更新语句。它只是插入全新的数据。
TL;DR: 好像跟外键有关。如果我删除它,那么我根本不会遇到任何死锁。但由于显而易见的原因,这不是一个可以接受的table解决方案。
给定以下table结构
CREATE TABLE [dbo].[IncomingFile]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
CONSTRAINT [PK_IncomingFile] PRIMARY KEY CLUSTERED([Id])
)
GO
CREATE TABLE [dbo].[IncomingFileEvent]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
[IncomingFileId] UNIQUEIDENTIFIER NOT NULL,
CONSTRAINT [PK_IncomingFileEvent] PRIMARY KEY CLUSTERED([Id]),
CONSTRAINT [FK_IncomingFileEvent_IncomingFileId]
FOREIGN KEY ([IncomingFileId])
REFERENCES [dbo].[IncomingFile] ([Id])
)
GO
当我遇到多个插入数据的并发任务时,我总是看到死锁。 READ_COMMITTED_SNAPSHOT
在我的数据库选项中启用(即使我没有阅读)。
这是将重现该问题的代码。如果您没有遇到问题,请增加程序顶部的 NumberOfTasksPerCpu
常量。
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace SqlServerDeadlockRepro
{
class Program
{
private const int NumberOfTasksPerCpu = 8; // Keep increasing this by one if you do not get a deadlock!
private const int NumberOfChildRows = 1_000;
private const string MSSqlConnectionString = "Server=DESKTOP-G05BF1U;Database=EFCoreConcurrencyTest;Trusted_Connection=True;";
private static int NumberOfConcurrentTasks;
static async Task Main(string[] args)
{
NumberOfConcurrentTasks = Environment.ProcessorCount * NumberOfTasksPerCpu;
var readySignals = new Queue<ManualResetEvent>();
var trigger = new ManualResetEvent(false);
var processingTasks = new List<Task>();
for (int index = 0; index < NumberOfConcurrentTasks; index++)
{
var readySignal = new ManualResetEvent(false);
readySignals.Enqueue(readySignal);
var task = CreateDataWithSqlCommand(trigger, readySignal);
processingTasks.Add(task);
}
Console.WriteLine("Waiting for tasks to become ready");
while (readySignals.Count > 0)
{
var readySignalBatch = new List<WaitHandle>();
for(int readySignalCount = 0; readySignals.Count > 0 && readySignalCount < 64; readySignalCount++)
{
readySignalBatch.Add(readySignals.Dequeue());
}
WaitHandle.WaitAll(readySignalBatch.ToArray());
}
Console.WriteLine("Saving data");
var sw = Stopwatch.StartNew();
trigger.Set();
await Task.WhenAll(processingTasks.ToArray());
sw.Stop();
Console.WriteLine("Finished - " + sw.ElapsedMilliseconds);
}
private static int TaskNumber = 0;
private static async Task CreateDataWithSqlCommand(ManualResetEvent trigger, ManualResetEvent readySignal)
{
await Task.Yield();
using var connection = new SqlConnection(MSSqlConnectionString);
await connection.OpenAsync().ConfigureAwait(false);
var transaction = (SqlTransaction)await connection.BeginTransactionAsync(System.Data.IsolationLevel.ReadCommitted).ConfigureAwait(false);
Console.WriteLine("Task " + Interlocked.Increment(ref TaskNumber) + $" of {NumberOfConcurrentTasks} ready ");
readySignal.Set();
trigger.WaitOne();
Guid parentId = Guid.NewGuid();
string fileCommandSql = "insert into IncomingFile (Id) values (@Id)";
using var fileCommand = new SqlCommand(fileCommandSql, connection, transaction);
fileCommand.Parameters.Add("@Id", System.Data.SqlDbType.UniqueIdentifier).Value = parentId;
await fileCommand.ExecuteNonQueryAsync().ConfigureAwait(false);
using var fileEventCommand = new SqlCommand
{
Connection = connection,
Transaction = transaction
};
var commandTextBulder = new StringBuilder("INSERT INTO [IncomingFileEvent] ([Id], [IncomingFileId]) VALUES ");
for (var i = 1; i <= NumberOfChildRows * 2; i += 2)
{
commandTextBulder.Append($"(@p{i}, @p{i + 1})");
if (i < NumberOfChildRows * 2 - 1)
commandTextBulder.Append(',');
fileEventCommand.Parameters.AddWithValue($"@p{i}", Guid.NewGuid());
fileEventCommand.Parameters.AddWithValue($"@p{i + 1}", parentId);
}
fileEventCommand.CommandText = commandTextBulder.ToString();
await fileEventCommand.ExecuteNonQueryAsync().ConfigureAwait(false);
await transaction.CommitAsync().ConfigureAwait(false);
}
}
}
更新
还尝试制作主键 NONCLUSTERED
并根据当前日期和时间添加 CLUSTERED
索引。
CREATE TABLE [dbo].[IncomingFile]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
[CreatedUtc] DateTime2 DEFAULT GETDATE(),
CONSTRAINT [PK_IncomingFile] PRIMARY KEY NONCLUSTERED([Id])
)
GO
CREATE CLUSTERED INDEX [IX_IncomingFile_CreatedUtc] on [dbo].[IncomingFile]([CreatedUtc])
GO
CREATE TABLE [dbo].[IncomingFileEvent]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[ConcurrencyVersion] RowVersion NOT NULL,
[IncomingFileId] UNIQUEIDENTIFIER NOT NULL,
[CreatedUtc] DateTime2 DEFAULT GETDATE(),
CONSTRAINT [PK_IncomingFileEvent] PRIMARY KEY NONCLUSTERED([Id]),
CONSTRAINT [FK_IncomingFileEvent_IncomingFileId]
FOREIGN KEY ([IncomingFileId])
REFERENCES [dbo].[IncomingFile] ([Id])
)
GO
CREATE CLUSTERED INDEX [IX_IncomingFileEvent_CreatedUtc] on [dbo].[IncomingFileEvent]([CreatedUtc])
GO
更新 2
我尝试了从 here 获取的顺序 guid,但没有任何区别。
更新 3
好像跟外键有关。如果我删除它,那么我根本不会遇到任何死锁。
更新 4
Sql 服务器产品组的回复和一些建议已发布在我原来的 github 问题上。
https://github.com/dotnet/efcore/issues/21899#issuecomment-683404734
死锁是由于检查参照完整性所需的执行计划造成的。将大量 (1K) 行插入相关 IncomingFileEvent
table 时,将执行 IncomingFile
table 的完整 table 扫描。扫描获取一个共享 table 锁,该锁在事务期间持有,当不同的会话各自持有刚插入的 IncomingFile
行上的独占行锁并被另一个独占会话阻塞时,会导致死锁行锁。
下面是显示此内容的执行计划:
避免死锁的一种方法是在 IncomingFileEvent
插入查询上使用 OPTION (LOOP JOIN)
查询提示:
var commandTextBulder = new StringBuilder("INSERT INTO [IncomingFileEvent] ([Id], [IncomingFileId]) VALUES ");
for (var i = 1; i <= NumberOfChildRows * 2; i += 2)
{
commandTextBulder.Append($"(@p{i}, @p{i + 1})");
if (i < NumberOfChildRows * 2 - 1)
commandTextBulder.Append(',');
fileEventCommand.Parameters.AddWithValue($"@p{i}", Guid.NewGuid());
fileEventCommand.Parameters.AddWithValue($"@p{i + 1}", parentId);
}
commandTextBulder.Append(" OPTION (LOOP JOIN);");
这是带有提示的计划:
附带说明一下,考虑将现有主键更改为以下主键。从数据建模的角度来看(识别关系)这是更正确的,并且会提高插入和选择的性能,因为相关行在物理上聚集在一起。
CONSTRAINT [PK_IncomingFileEvent] PRIMARY KEY CLUSTERED(IncomingFileId, Id)
我编写了以下扩展来解决 EF Core 的问题。
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
base.OnConfiguring(options);
options.UseLoopJoinQueries();
}
使用此代码...
public static class UseLoopJoinQueriesExtension
{
public static DbContextOptionsBuilder UseLoopJoinQueries(this DbContextOptionsBuilder builder)
{
if (builder is null)
throw new ArgumentNullException(nameof(builder));
builder.AddInterceptors(new OptionLoopJoinCommandInterceptor());
return builder;
}
}
internal class OptionLoopJoinCommandInterceptor : DbCommandInterceptor
{
public override Task<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
{
AppendOptionToSql(command);
return Task.FromResult(result);
}
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
AppendOptionToSql(command);
return result;
}
private static void AppendOptionToSql(DbCommand command)
{
const string OPTION_TEXT = " OPTION (LOOP JOIN)";
string[] commands = command.CommandText.Split(";");
for (int index = 0; index < commands.Length; index++)
{
string sql = commands[index].Trim();
if (sql.StartsWith("insert into ", StringComparison.InvariantCultureIgnoreCase)
|| sql.StartsWith("select ", StringComparison.InvariantCultureIgnoreCase)
|| sql.StartsWith("delete ", StringComparison.InvariantCultureIgnoreCase)
|| sql.StartsWith("merge ", StringComparison.InvariantCultureIgnoreCase))
{
commands[index] += OPTION_TEXT;
}
}
#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
command.CommandText = string.Join(";\r\n", commands);
#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
}
}