简体   繁体   English

SQL 事务死锁。 不知道为什么

[英]SQL Transaction Deadlock. No Idea Why

I have a Web API that has a method that has been working for several months without any issues ( PurchaseController ).我有一个 Web API 有一个已经工作了几个月没有任何问题的方法( PurchaseController )。 The list passed in was always around 100 or less.传入的列表总是在 100 左右或更少。 A few days ago, the volume spiked to around 600 items in the list which resulted in the app that passes the list getting a timeout instead of a success/failure response.几天前,列表中的数量飙升至大约 600 项,导致通过列表的应用程序获得超时而不是成功/失败响应。 It also seemed to make all other Web API methods run very slow until this method had finished running.它似乎也使所有其他 Web API 方法运行非常缓慢,直到该方法完成运行。 So I modified the app code to break up the submissions into a configurable number of items at a time (currently set to 20 for the purpose of testing).所以我修改了应用程序代码,一次将提交分解为可配置数量的项目(目前为测试目的设置为 20)。 I also modified the way the list was organized so that I could wrap each purchase in its own SQL transaction, in the hopes that this would allow other API methods to run without interruption.我还修改了列表的组织方式,以便我可以将每次购买包装在其自己的 SQL 事务中,希望这将允许其他 API 方法不间断地运行。

However, once these changes were made, I ran into frequent errors like this when PurchaseController was called:但是,一旦进行了这些更改,我在调用PurchaseController时经常遇到类似这样的错误:

Transaction (Process ID 62) was deadlocked on lock resources with another process and has been chosen as the deadlock victim.事务(进程 ID 62)与另一个进程在锁资源上死锁,并已被选为死锁牺牲品。 Rerun the transaction.重新运行事务。

The original PurchaseController code:原始PurchaseController代码:

public class PurchaseController : ControllerBase
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="Purchases"></param>
    /// <returns></returns>
    public JsonResult<APIResponse> Put([FromBody]CPurchases Purchases)
    {
        if (!ValidateCredentials(Request, this.ServiceName))
        {
            throw new HttpResponseException(HttpStatusCode.Unauthorized);
        }

        APIResponse response = new APIResponse();

        try
        {
            using (var dbTransaction = Members.Database.BeginTransaction())
            {
                response = base.RecordEventRegistrations(Purchases.EventRegistrations);

                if (!response.IsSuccess)
                    throw new Exception(response.Error);

                response = base.RecordPublicationPurchases(Purchases.PublicationPurchases);

                if (!response.IsSuccess)
                    throw new Exception(response.Error);

                response = base.RecordSubscriptionPurchases(Purchases.SubscriptionPurchases);

                if (!response.IsSuccess)
                    throw new Exception(response.Error);

                response = base.RecordTransactions(Purchases.Transactions);

                if (!response.IsSuccess)
                    throw new Exception(response.Error);

                dbTransaction.Commit();
            }
        }
        catch (Exception ex)
        {
            Utilities.Logging.LogToEventLog(ex);
            response.IsSuccess = false;
            response.Error = Utilities.Logging.GetInnermostException(ex);
        }

        return Json(response);
    }
}

The new code that ran into deadlocks on virtually every call:几乎每次调用都会遇到死锁的新代码:

public class PurchaseController : ControllerBase
{
    /// <summary>
    /// </summary>
    /// <param name="Purchases"></param>
    /// <returns></returns>
    public JsonResult<APIResponse> Put([FromBody]List<PurchaseDetails> Purchases)
    {
        if (!ValidateCredentials(Request, this.ServiceName))
        {
            throw new HttpResponseException(HttpStatusCode.Unauthorized);
        }

        APIResponse response = new APIResponse();

        try
        {
            using (var dbTransaction = Members.Database.BeginTransaction())
            {
                foreach (var Purchase in Purchases)
                {
                    response = base.RecordEventRegistrations(Purchase.EventRegistrations);

                    if (!response.IsSuccess)
                        throw new Exception(response.Error);

                    response = base.RecordPublicationPurchases(Purchase.PublicationPurchases);

                    if (!response.IsSuccess)
                        throw new Exception(response.Error);

                    response = base.RecordSubscriptionPurchases(Purchase.SubscriptionPurchases);

                    if (!response.IsSuccess)
                        throw new Exception(response.Error);

                    response = base.RecordTransactions(Purchase.PaymentTransactions);

                    if (!response.IsSuccess)
                        throw new Exception(response.Error);

                    dbTransaction.Commit();
                }
            }
        }
        catch (Exception ex)
        {
            Utilities.Logging.LogToEventLog(ex);
            response.IsSuccess = false;
            response.Error = Utilities.Logging.GetInnermostException(ex);
        }

        return Json(response);
    }
}  

I later moved the SQL transaction outside the foreach like this:我后来将 SQL 事务移到了 foreach 之外,如下所示:

foreach (var Purchase in Purchases)
{
    using (var dbTransaction = Members.Database.BeginTransaction())
    {

and this made a huge difference in the frequency of deadlocks, but did not eliminate them.这对死锁的频率产生了巨大的影响,但并没有消除它们。 I also added System.Data.IsolationLevel.RepeatableRead to the SQL transaction but it seemed to make no difference.我还将System.Data.IsolationLevel.RepeatableRead添加到 SQL 事务中,但似乎没有什么区别。

The code calling this method changed from:调用此方法的代码从:

public static APIResponse SavePurchases(IEnumerable<EventRegistration> Registrations, IEnumerable<PublicationPurchase> PublicationPurchases, IEnumerable<SubscriptionPurchase> SubscriptionPurchases, IEnumerable<PaymentTransactionModel> Transactions)
{
    if (Registrations.Count() == 0 && PublicationPurchases.Count() == 0 && SubscriptionPurchases.Count() == 0 && Transactions.Count() == 0)
        return new APIResponse() { IsSuccess = true };

    CPurchases Purchases = new CPurchases()
        {
            EventRegistrations = Registrations,
            PublicationPurchases = PublicationPurchases,
            SubscriptionPurchases = SubscriptionPurchases,
            Transactions = Transactions
        };

    return PutAPI("/Purchase", Purchases);
}

to

public static APIResponse SavePurchases(IEnumerable<PurchaseDetails> Purchases)
{
    int PageSize = AppSettings.MaxNumberOfPurchasesPerAPIBatch;

    APIResponse Response = new APIResponse() { IsSuccess = true };

    List<PurchaseDetails> Page;

    for (int i = 0; i < Purchases.Count(); i = i + PageSize)
    {
        Page = Purchases.Skip(i).Take(PageSize).ToList();

        if (Page.Any())
        {
            Response = PutAPI("/Purchase", Page);

            if (!Response.IsSuccess)
                break;
        }
    }

    return Response;
}

In addition, it is worth noting that the table the RecordEventRegistrations modifies has a single trigger to enforce that the varchar event number is in all caps.此外,值得注意的是, RecordEventRegistrations修改的表有一个触发器来强制 varchar 事件编号全部大写。 This was put in place when the original code went live.这是在原始代码上线时实施的。 And all database changes are done using Entity Framework 6.并且所有数据库更改都是使用 Entity Framework 6 完成的。

No other code changes to the database, the app, or the Web API have been made in the past 2 weeks, so I'm pretty sure the problem is due to these code changes but I can't seem to figure out what or why.在过去 2 周内没有对数据库、应用程序或 Web API 进行其他代码更改,所以我很确定问题是由于这些代码更改造成的,但我似乎无法弄清楚是什么或为什么.

UPDATE: I've been trying to get the deadlock graph for several hours but my account didn't have the correct permissions.更新:我一直在尝试获取死锁图几个小时,但我的帐户没有正确的权限。 Now that I have it I believe the problem is in the trigger, if I'm reading the graph properly.现在我有了它,如果我正确阅读图表,我相信问题出在触发器上。 I've temporarily disabled the trigger and it seems to have resolved the deadlocks.我暂时禁用了触发器,它似乎已经解决了死锁。 But since the trigger has been unchanged for the past 6+ months, I'm guessing it has something to do with the volume of insert/updates over the last couple of days?但是由于触发器在过去 6 个月内一直没有变化,我猜这与过去几天的插入/更新量有关吗?

xml_report  
<deadlock>
    <victim-list>
        <victimProcess id="process23368b65468"/>
    </victim-list>
    <process-list>
        <process id="process23368b65468" taskpriority="0" logused="248" waitresource="PAGE: 6:1:84432 " waittime="3847" ownerId="22806353" transactionname="user_transaction" lasttranstarted="2020-12-30T10:33:09.433" XDES="0x2360aa86408" lockMode="U" schedulerid="4" kpid="3916" status="suspended" spid="54" sbid="2" ecid="0" priority="0" trancount="2" lastbatchstarted="2020-12-30T10:33:09.437" lastbatchcompleted="2020-12-30T10:33:09.420" lastattention="1900-01-01T00:00:00.420" clientapp="EntityFramework" hostname="***-API-01" hostpid="1400" loginname="sa" isolationlevel="read committed (2)" xactid="22806353" currentdb="6" lockTimeout="4294967295" clientoption1="671219744" clientoption2="128056">
            <executionStack>
                <frame procname="members.dbo.tr_event_registration" line="8" stmtstart="332" stmtend="584" sqlhandle="0x03000600e3d0825d1a48970094ac000000000000000000000000000000000000000000000000000000000000">  UPDATE [Event_Registration]  SET event_number = UPPER(event_number)  WHERE  event_number in (select event_number from inserted    </frame>
                <frame procname="adhoc" line="1" stmtstart="156" stmtend="506" sqlhandle="0x0200000015ef0508db49eeb79e583b351130d3afdb0ebac40000000000000000000000000000000000000000">  unknown    </frame>
                <frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">  unknown    </frame>
            </executionStack>
            <inputbuf>  (@0 datetime2(7),@1 char(2),@2 datetime2(7),@3 datetime2(7),@4 int,@5 char(5))UPDATE [dbo].[Event_Registration]  SET [registration_date] = @0, [source] = @1, [payment_date] = @2, [time_stamp] = @3  WHERE (([entity_number] = @4) AND ([event_number] = @5))     </inputbuf>
        </process>
        <process id="process233a1618ca8" taskpriority="0" logused="328" waitresource="PAGE: 6:1:1848653 " waittime="3824" ownerId="22806350" transactionname="UPDATE" lasttranstarted="2020-12-30T10:33:09.383" XDES="0x2360db58e58" lockMode="U" schedulerid="1" kpid="6096" status="suspended" spid="90" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2020-12-30T10:33:09.380" lastbatchcompleted="2020-12-30T10:33:09.393" lastattention="1900-01-01T00:00:00.393" clientapp=".Net SqlClient Data Provider" hostname="***-MBR-01" hostpid="3936" loginname="sa" isolationlevel="read committed (2)" xactid="22806350" currentdb="6" lockTimeout="4294967295" clientoption1="671219744" clientoption2="128056">
            <executionStack>
                <frame procname="members.dbo.tr_event_registration" line="8" stmtstart="332" stmtend="584" sqlhandle="0x03000600e3d0825d1a48970094ac000000000000000000000000000000000000000000000000000000000000">  UPDATE [Event_Registration]  SET event_number = UPPER(event_number)  WHERE  event_number in (select event_number from inserted    </frame>
                <frame procname="adhoc" line="1" stmtstart="714" stmtend="1696" sqlhandle="0x02000000f367b4079d5a8152b57beb111052747ccde62e3d0000000000000000000000000000000000000000">  unknown    </frame>
                <frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000">  unknown    </frame>
            </executionStack>
            <inputbuf>  (@is_canceled bit,@source nvarchar(1),@event_number nvarchar(5),@entity_number int,@registration_date datetime,@cancel_date nvarchar(4000),@fee_code nvarchar(2),@amount_paid float,@pay_status nvarchar(1),@show_up nvarchar(1),@payment_date nvarchar(10),@payment_method nvarchar(6),@hours_override_reason nvarchar(4000),@quantity int,@user_stamp nvarchar(11))update event_registration set registration_date = @registration_date, cancel_date = @cancel_date, is_canceled = @is_canceled, fee_code = @fee_code, amount_paid = @amount_paid, pay_status = @pay_status, show_up = @show_up, payment_date = @payment_date, payment_method = @payment_method, hours_override_reason = @hours_override_reason, quantity = @quantity, user_stamp = @user_stamp, source = @source, time_stamp = getdate() where event_number = @event_number and entity_number = @entity_number; IF @@ROWCOUNT = 0 BEGIN insert into event_registration (event_number, entity_number, registration_date, cancel_date, is_canceled, fee_code, amount_paid, pay_status, show_up   </inputbuf>
        </process>
    </process-list>
    <resource-list>
        <pagelock fileid="1" pageid="84432" dbid="6" subresource="FULL" objectname="members.dbo.Event_Registration" id="lock233a5ad4d00" mode="U" associatedObjectId="72057594463256576">
            <owner-list>
                <owner id="process233a1618ca8" mode="U"/>
            </owner-list>
            <waiter-list>
                <waiter id="process23368b65468" mode="U" requestType="wait"/>
            </waiter-list>
        </pagelock>
        <pagelock fileid="1" pageid="1848653" dbid="6" subresource="FULL" objectname="members.dbo.Event_Registration" id="lock233a5858d00" mode="IX" associatedObjectId="72057594463256576">
            <owner-list>
                <owner id="process23368b65468" mode="IX"/>
            </owner-list>
            <waiter-list>
                <waiter id="process233a1618ca8" mode="U" requestType="wait"/>
            </waiter-list>
        </pagelock>
    </resource-list>
</deadlock>

The trigger is:触发器是:

ALTER Trigger [dbo].[tr_event_registration]
on [dbo].[Event_Registration]
AFTER insert, update
as
if ((trigger_nestlevel() > 1) or (@@rowcount = 0))
    return
UPDATE [Event_Registration]
SET event_number = UPPER(event_number)
WHERE
event_number in (select event_number from inserted);

Replace the trigger with a CHECK constraint.将触发器替换为 CHECK 约束。 Updating a key value every time after inserting a row is wasteful, and is the cause of excessive locking.每次插入一行后更新一个键值是浪费的,并且是过度锁定的原因。

primary key which is a composite made up of entity_number ( int ) and event_number ( char(5) )主键是由 entity_number ( int ) 和 event_number ( char(5) ) 组成的组合

Then your trigger requires a scan and will update all the rows for the entity_number on every insert and update.然后,您的触发器需要扫描,并将在每次插入和更新时更新 entity_number 的所有行。

Change it to conditionally update only the rows affected by the triggering statement.将其更改为仅有条件地更新受触发语句影响的行。 Something like:就像是:

ALTER Trigger [dbo].[tr_event_registration]
on [dbo].[Event_Registration]
AFTER insert, update
as
begin
   if ((trigger_nestlevel() > 1) or (@@rowcount = 0))
      return;

   
   
   UPDATE [Event_Registration] e
   SET event_number = UPPER(event_number)
   WHERE event_number != upper(event_number) COLLATE Latin1_General_CS_AS
   and EXISTS 
   (
     select * 
     from inserted i
     where i.event_number = e.event_number
       and i.entity_number = e.entity_number
   );
   
end 

Will these changes prevent or just reduce the likelihood of a deadlock?这些变化会防止或只是减少死锁的可能性吗?

Probably prevent.应该是预防吧。 Your deadlock included page locks, probably because the trigger was scanning and updating additional rows.您的死锁包括页面锁,可能是因为触发器正在扫描和更新其他行。 So any two sessions touching the same event_number would likely cause a deadlock.因此,任何两个接触相同 event_number 的会话都可能导致死锁。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM