简体   繁体   English

使用并发插入避免 SQL 中的死锁

[英]Avoiding Deadlocks in SQL with concurrent inserts

I have a process (Process A) which keeps adding records to a SQL table (Table A) (Direct inserts using stored procedure).我有一个进程(进程 A),它不断将记录添加到 SQL 表(表 A)(使用存储过程直接插入)。 It is a continuous process which reads the requests and writes to a table.这是一个读取请求并写入表的连续过程。 There is no pattern to how the requests come.请求如何来没有模式。 The max requests per day is around 100K.每天的最大请求数约为 100K。

Once the requests come in, I need to do some processing on those requests.一旦请求进来,我需要对这些请求进行一些处理。 These are currently done in user desktops (due to licensing issues).这些目前在用户桌面中完成(由于许可问题)。 The way I am currently doing is having an executable (Process B) run on each user and as and when requests come in to the table, this process reads and does some work and writes to the same table.我目前正在做的方式是在每个用户上运行一个可执行文件(进程 B),并且当请求进入表时,该进程读取并执行一些工作并写入同一个表。 So Table is read/written by multiple processes.所以 Table 由多个进程读取/写入。 The process B has the following logic进程B有如下逻辑

  • Get records that have not been processed by another user and is not being currently processed by another user获取未被其他用户处理且当前未被其他用户处理的记录

    • Lock the records for this run by marking a flag isProcessing (C# LINQ through SP).通过标记标志 isProcessing(C# LINQ 到 SP)来锁定此运行的记录。 This is a single SQL transaction ie lock records and get them for processing is wrapped in a transaction这是一个单独的 SQL 事务,即锁定记录并获取它们以进行处理被包装在一个事务中
  • process the records.处理记录。 This is where the calculation occurs.这是计算发生的地方。 No db work here.这里没有数据库工作。

  • insert/update records in table A (C# LINQ through db.submitchanges).在表 A 中插入/更新记录(C# LINQ 通过 db.submitchanges)。 This is where the deadlock occurs.这就是发生死锁的地方。 This is a separate SQL transaction.这是一个单独的 SQL 事务。

Occasionally, I see deadlocks when writing to the table.有时,我在写入表时会看到死锁。 This SQL Server 2008 (with isolation level Read committed).此 SQL Server 2008(具有已提交的隔离级别)。 Access to SQL is done by both Stored procedures and direct C# Linq Queries.对 SQL 的访问由存储过程和直接 C# Linq 查询完成。 Question is how to avoid the deadlocks.问题是如何避免死锁。 Is there a better overall architecture ?有更好的整体架构吗? Maybe, instead of all these child processes writing to the table independently, I should send them to a service which queues them up and writes to the table ?.也许,不是所有这些子进程都独立写入表,而是应该将它们发送到一个服务,该服务将它们排队并写入表? I know it is tough to answer without having all the code (just too many to show) but hopefully I have explained it and I will happy to answer any specific questions.我知道如果没有所有代码(太多无法显示)很难回答,但希望我已经解释过了,我很乐意回答任何具体问题。

This is a representative table structure.这是一个有代表性的表结构。

    CREATE TABLE [dbo].[tbl_data](
[tbl_id] [nvarchar](50) NOT NULL,
[xml_data] [xml] NULL, -- where output will be stored
[error_message] [nvarchar](250) NULL,
[last_processed_date] [datetime] NULL,
[last_processed_by] [nvarchar](50) NULL,
[processing_id] [uniqueidentifier] NULL,
[processing_start_date] [datetime] NULL,
[create_date] [datetime] NOT NULL,
[processing_user] [nvarchar](50) NULL,
    CONSTRAINT [PK_tbl_data] PRIMARY KEY CLUSTERED 
    (
[tbl_id] ASC,
[create_date] ASC
    ) ON [PRIMARY]

This is the proc that gets the data for processing.这是获取数据进行处理的过程。

    begin tran
            -- clear processing records that have been running for more than 6 minutes... they need to be reprocessed...
    update tbl_data set processing_id = null, processing_start_date = null
    where DATEDIFF(MINUTE, processing_start_date, GETDATE()) >=6

    DECLARE @myid uniqueidentifier = NEWID();

    declare @user_count int

    -- The literal number below is the max any user can process. The last_processed_by and last_processed_date are updated when a record has been processed
    select @user_count = 5000 - count(*) from  tbl_data where last_processed_by = @user_name and  DATEDIFF(dd, last_processed_date, GETDATE()) = 0

    IF (@user_count > 1000) 
        SET @user_count = 1000 -- no more than 1000 requests in each batch.

    if (@user_count < 0) 
        set @user_count = 0



    --mark the records as being processed
    update tbl_data set processing_id = @myid, processing_start_date = GETDATE(), processing_user = @user_name from tbl_data t1 join
    (
        select top (@user_count) tbl_id from tbl_data
        where 
            [enabled] = 1 and processing_id is null 
        and isnull(DATEDIFF(dd, last_processed_date, GETDATE()), 1) > 0 
        and isnull(DATEDIFF(dd, create_date, GETDATE()), 1) = 0 
    ) t2 on t1.tbl_id = t2.tbl_id

    -- get the records that have been marked
    select tbl_id from tbl_data where processing_id = @myid 

    commit tran

My guess is you are deadlocking on pages as concurrent updates are attempted.我的猜测是你在尝试并发更新时在页面上死锁。

With the nature of the updates and inserts (a sliding timeframe window based on getdate), it looks like a good partitioning scheme is difficult to implement.由于更新和插入的性质(基于 getdate 的滑动时间帧窗口),看起来一个好的分区方案很难实现。 Without it, I think your best option would be to implement an application level lock (the sql equivalent of a mutex) using sp_getapplock http://msdn.microsoft.com/en-us/library/ms189823(v=sql.100).aspx没有它,我认为您最好的选择是使用 sp_getapplock http://msdn.microsoft.com/en-us/library/ms189823(v=sql.100) 实现应用程序级锁(相当于互斥锁的 sql) .aspx

I lack the time right now to analyze your workload and find a true fix.我现在没有时间分析您的工作量并找到真正的解决方案。 So I'm going to add a different kind of answer: You can safely retry deadlocking transactions.所以我要添加另一种答案:您可以安全地重试死锁事务。 This problem can be fixed by just re-running the entire transactions.只需重新运行整个事务即可解决此问题。 Maybe a little delay needs to be inserted before a retry is attempted.在尝试重试之前,可能需要插入一点延迟。

Be sure to rerun the entire transaction, though, including any control flow that happens in the application.不过,请务必重新运行整个事务,包括应用程序中发生的任何控制流。 In case of a retry the data that was already read might have changed.在重试的情况下,已读取的数据可能已更改。

If retries are rare this is not a performance problem.如果重试很少,这不是性能问题。 You should probably log when a retry happened.您可能应该在重试发生时记录。

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

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