如何解决 SqlException 在锁定时死锁 |通信缓冲资源

How to troubleshoot SqlException deadlocked on lock | communication buffer resources

这个问题在 Whosebug 上已经有不同的版本,但是 none 帮助我弄清了问题的根源。所以,我在这里再次详细说明我的问题。

我们随机得到 Transaction (Process ID xx) was deadlocked on lock | communication buffer resources with another process and has been chosen as the deadlock victim. Rerun the transaction.。让我说清楚,这不是行或 table 级别锁定。我已经尝试了足够多的 guessed/random 东西;我需要有关如何解决通信缓冲区死锁问题的准确分步指南。

如果您对具体细节感兴趣,请继续阅读。

场景的具体细节:我们有一个非常简单的基于 Dapper ORM 的 C# .net 核心 Web API 接收请求并对托管的数据库执行 CRUD 操作在此 Microsoft Sql 服务器上。为此,连接管理器(注册为范围服务)在请求范围内打开一个新的 IDbConnection 连接;此连接用于执行删除、插入、更新或获取。对于 insert/update/delete C# 行看起来像这样 await connection.ExecuteAsync("<Create update or delete statement>", entity); 对于 GET 请求我们简单地 运行 await connection.QueryFirstOrDefaultAsync<TEntity>("<select statement>", entity); ;有 5 种类型的实体(都呈现简单的非关系 tables)。它们都是按 ID 增删改查的。

到目前为止已经尝试了什么

  1. MAXDOP=1 对 SQL 语句的查询提示
  2. 确保一种实体在给定时间点只有 1 个实体 CRUD。
  3. 正在重新启动 SQL server/application 实例
  4. 确保 ports/RAM/CPU/network 带宽不被耗尽
  5. 改变数据库 XXXXXX 集 READ_COMMITTED_SNAPSHOT ON/OFF
  6. 保持事务尽可能小
  7. 作为解决方法的持久重试策略(处理问题的随机瞬态性质)
  8. 每个实体类型单线程

服务器规格: 我们将 Microsoft Sql Server 2016 On Azure 托管在具有 64 个内核和 400GB RAM 的虚拟机中。此服务器上的通常工作负载是 10% CPU 和 30% RAM,偶尔会上升到 80% CPU 和 350GB RAM。在发生此问题的所有时间,CPU 使用率都低于 20%(大多数情况下在 10% 左右,只有一次 20%,RAM 在所有情况下都低于 30%)。

死锁XML 按照@Dan Guzman 的要求进行的事件

文件大小对于此 post 来说太大,因此创建了此 google 驱动器文件。请点击下面的link然后在右上角点击下载。这是一个 zip 文件。

https://drive.google.com/file/d/1oZ4dT8Yrd2uW2oBqBy9XK_laq7ftGzFJ/view?usp=sharing

死锁通常是需要查询和索引调优的症状。下面是来自死锁跟踪的示例查询,它表明了死锁的根本原因:

<inputbuf>
@SomeStatus1 nvarchar(4000),@ProductName nvarchar(4000),@ProductNameSide nvarchar(4000),@BayNo nvarchar(4000),@CreatedDateTime datetime,@EffectiveDate datetime,@ForSaleFrom datetime,@ForSaleTo datetime,@SetupInfoNode nvarchar(4000),@LocationNumber nvarchar(4000),@AverageProductPrice decimal(3,2),@NetAverageCost decimal(3,1),@FocustProductType nvarchar(4000),@IsProduceCode nvarchar(4000),@ActivationIndicator nvarchar(4000),@ResourceType nvarchar(4000),@ProductIdentifierNumber nvarchar(4000),@SellingStatus nvarchar(4000),@SectionId nvarchar(4000),@SectionName nvarchar(4000),@SellPriceGroup nvarchar(4000),@ShelfCapacity decimal(1,0),@SellingPriceTaxExclu decimal(2,0),@SellingPriceTaxInclu decimal(2,0),@UnitToSell nvarchar(4000),@VendorNumber nvarchar(4000),@PastDate datetime,@PastPrice decimal(29,0))
UPDATE dbo.ProductPricingTable 
SET SellingPriceTaxExclu = @SellingPriceTaxExclu, SellingPriceTaxInclu = @SellingPriceTaxInclu, 
SellPriceGroup = @SellPriceGroup, 
ActivationIndicator = @ActivationIndicator, 
IsProduceCode = @IsProduceCode, 
EffectiveDate = @EffectiveDate, 
NetCos
</inputbuf>

虽然SQL语句文本被截断了,但它确实表明所有参数声明都是nvarchar(4000)(ORM的常见问题)。当 join/where 子句中引用的列类型不同时,这可能会阻止索引的有效使用,从而导致在并发查询期间导致死锁的全扫描。

更改参数类型以匹配引用列的类型并检查执行计划的效率。

@DanGuzman 提供了帮助,所以我不得不 upvote/choose 他的回答作为接受的答案。但是,我想总结一下这里发生了什么,我学到了什么,以及如何解决通信缓冲区死锁(或与此相关的任何死锁)的逐步方法。

步骤 - 1
拉取死锁报告。我使用了以下查询,但您也可以使用@DanGuzman 建议的查询(在这个问题的评论部分)。

SELECT
   xed.value('@timestamp', 'datetime2(3)') as CreationDate,
   xed.query('.') AS XEvent
FROM
(
   SELECT CAST([target_data] AS XML) AS TargetData
   FROM sys.dm_xe_session_targets AS st
      INNER JOIN sys.dm_xe_sessions AS s
         ON s.address = st.event_session_address
      WHERE s.name = N'system_health'
         AND st.target_name = N'ring_buffer'
) AS Data
CROSS APPLY TargetData.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData (xed)
ORDER BY CreationDate DESC

步骤 - 2
找到与您的 sql 异常 timing/data 相对应的死锁事件。然后结合 Detecting and Ending Deadlocks 指南阅读此报告,了解死锁问题的根本原因。在我的例子中,我在通信缓冲区上遇到了死锁,因此根据本指南,内存(检测和结束死锁指南的 Memory 部分)一定是导致问题的原因。正如丹指出的那样,在我的例子中,以下查询出现在死锁报告中,该报告使用了太多缓冲区(由于查询效率低下)。那么什么是通信缓冲区死锁呢?好吧,如果这个查询需要太多的缓冲区来完成它的执行,那么两个这样的查询可以同时开始执行并开始声明他们需要的缓冲区,但在某些时候可用的缓冲区可能不够,他们将不得不等待更多缓冲区从其他查询的执行中释放出来。所以两个查询都等待对方完成,希望能释放更多的缓冲区。这可能导致缓冲区死锁(根据指南的内存部分)

<inputbuf>
@SomeStatus1 nvarchar(4000),@ProductName nvarchar(4000),@ProductNameSide nvarchar(4000),@BayNo nvarchar(4000),@CreatedDateTime datetime,@EffectiveDate datetime,@ForSaleFrom datetime,@ForSaleTo datetime,@SetupInfoNode nvarchar(4000),@LocationNumber nvarchar(4000),@AverageProductPrice decimal(3,2),@NetAverageCost decimal(3,1),@FocustProductType nvarchar(4000),@IsProduceCode nvarchar(4000),@ActivationIndicator nvarchar(4000),@ResourceType nvarchar(4000),@ProductIdentifierNumber nvarchar(4000),@SellingStatus nvarchar(4000),@SectionId nvarchar(4000),@SectionName nvarchar(4000),@SellPriceGroup nvarchar(4000),@ShelfCapacity decimal(1,0),@SellingPriceTaxExclu decimal(2,0),@SellingPriceTaxInclu decimal(2,0),@UnitToSell nvarchar(4000),@VendorNumber nvarchar(4000),@PastDate datetime,@PastPrice decimal(29,0))
UPDATE dbo.ProductPricingTable 
SET SellingPriceTaxExclu = @SellingPriceTaxExclu, SellingPriceTaxInclu = @SellingPriceTaxInclu, 
SellPriceGroup = @SellPriceGroup, 
ActivationIndicator = @ActivationIndicator, 
IsProduceCode = @IsProduceCode, 
EffectiveDate = @EffectiveDate, 
NetCos
</inputbuf>

步骤 3(修复)
等待 !!!!但我用的是 Dapper。那它怎么会把我的查询变成这样一个致命的查询呢? Well Dapper 非常适合大多数情况下的开箱即用默认设置,但是,很明显,在我的情况下,这个默认的 4000 nvarchar 杀死了它(请阅读 Dan 的回答以了解这样的查询如何导致问题)。正如 Dan 所建议的,我从这样的输入实体自动构建参数 await connection.ExecuteAsync("<Create update or delete statement>", entity);,其中 entity 是 C# 模型 class 的一个实例。我更改了它的自定义参数,如下所示。 (为简单起见,我只添加了一个参数,但您可以使用所有必需的参数)

            var parameters = new DynamicParameters();
            parameters.Add("Reference", entity.Reference, DbType.AnsiString, size: 18 );
await connection.ExecuteAsync("<Create update or delete statement>", parameters );

我可以在探查器中看到请求现在具有完全匹配的类型列参数类型。就是这样,此修复使问题消失了。谢谢丹。

结论
我可以得出结论,在我的案例中,通信缓冲区发生死锁是因为错误的查询占用了太多缓冲区来执行。之所以如此,是因为我盲目地使用了默认的 Dapper 参数生成器。使用 Dapper 的自定义参数构建器解决了这个问题。