如何排查SqlException锁上死锁| 通信缓冲区资源

sbp*_*sbp 3 c# sql-server deadlock sqlexception database-deadlocks

stackoverflow 上已经有这个问题的不同版本,但没有一个版本可以帮助我深入了解问题。因此,我在这里再次详细介绍我的问题。

我们一直随机得到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.. 让我澄清一下,这不是行级或表级锁定。我已经尝试了足够多的猜测/随机的事情;我需要有关如何解决通信缓冲区死锁问题的准确分步指南。

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

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

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

  1. MAXDOP=1 SQL 语句的查询提示
  2. 确保在给定时间点仅对一种实体进行 1 个实体 CRUD。
  3. 重新启动 SQL 服务器/应用程序实例
  4. 确保端口/RAM/CPU/网络带宽不被耗尽
  5. 更改数据库 XXXXXX SET 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%)。

根据 @Dan Guzman 的请求发生死锁 XML 事件

文件大小对于这篇文章来说太大了,因此创建了这个谷歌驱动器文件。请点击以下链接,然后点击右上角的下载。它是一个 zip 文件。

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

sbp*_*sbp 6

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

步骤 - 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
Run Code Online (Sandbox Code Playgroud)

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

<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>
Run Code Online (Sandbox Code Playgroud)

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

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

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

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