Recently a clients website has been receiving many deadlocks.
The website and CMS is custom built on asp.net MVC 3. The database is SQL Server 2008.
I believe this started occurring when there was an increase in CMS traffic making updates to records. An update to one item in our CMS results in updates across several tables which is all contained in a plain new TransactionScope()
I have looked at the deadlock XML but I am unsure exactly why the deadlock is occurring. I am looking for any help in explaining why the dead lock is occurring and any suggestions to resolved the deadlocks.
<deadlock-list>
<deadlock victim="processa94e3948">
<process-list>
<process id="processa94e3948" taskpriority="0" logused="0" waitresource="KEY: 7:72057602921267200 (e70029cc925c)" waittime="2413" ownerId="122355021476" transactionname="INSERT" lasttranstarted="2015-02-17T17:00:57.210" XDES="0x3b46199f0" lockMode="RangeS-S" schedulerid="5" kpid="344716" status="suspended" spid="109" sbid="2" ecid="0" priority="0" trancount="0" lastbatchstarted="2015-02-17T17:00:57.090" lastbatchcompleted="2015-02-17T17:00:57.090" clientapp=".Net SqlClient Data Provider" hostname="SWIWEB02" hostpid="8560" loginname="TravelWisconsinWeb" isolationlevel="serializable (4)" xactid="122355021476" currentdb="7" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
<executionStack>
<frame procname="TravelWisconsinWeb.dbo.BlogArticleGetBlogArticlesByFiltersWithPagingCount" line="75" stmtstart="4086" stmtend="4958" sqlhandle="0x030007005488c563185701010da300000100000000000000">
insert into @taggedBlogs
select distinct v.BaseContentId
from Tags t
inner join ContentPages cp on cp.Id = t.ContentPageId
inner join BlogArticles ba on ba.ContentPageId = cp.Id and ba.BlogArticleTypeId = @BlogArticleTypeId
inner join Versions v on (v.ContentId = ba.Id or v.BaseContentId = ba.Id) and v.ContentTypeId = @blogContentTypeId
where t.Tag in (SELECT Data FROM dbo.Split(@Tags, ',')); </frame>
</executionStack>
<inputbuf>
Proc [Database Id = 7 Object Id = 1673889876] </inputbuf>
</process>
<process id="processd734c8" taskpriority="0" logused="34628" waitresource="OBJECT: 7:1179111937:0 " waittime="2692" ownerId="122355020379" transactionname="user_transaction" lasttranstarted="2015-02-17T17:00:56.370" XDES="0x3031623b0" lockMode="IX" schedulerid="3" kpid="345824" status="suspended" spid="54" sbid="2" ecid="0" priority="0" trancount="2" lastbatchstarted="2015-02-17T17:00:57.290" lastbatchcompleted="2015-02-17T17:00:57.290" clientapp=".Net SqlClient Data Provider" hostname="SWIWEB01" hostpid="9128" loginname="TravelWisconsinWeb" isolationlevel="serializable (4)" xactid="122355020379" currentdb="7" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="TravelWisconsinWeb.dbo.VersionInsert" line="21" stmtstart="892" stmtend="1468" sqlhandle="0x03000700d168c661f25401010da300000100000000000000">
insert into Versions
(BaseContentId, ContentId, StatusId, SiteId, IsPublished, ModifiedByUserId, ModifiedDate, ModifiedMessage, ContentTypeId)
values (@BaseContentId, @ContentId, @StatusId, @SiteId, @IsPublished, @ModifiedByUserId, GETDATE(), @ModifiedMessage, @ContentTypeId); </frame>
</executionStack>
<inputbuf>
Proc [Database Id = 7 Object Id = 1640392913] </inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057602921267200" dbid="7" objectname="TravelWisconsinWeb.dbo.ContentPages" indexname="index_treenodeid" id="lock101eedd80" mode="X" associatedObjectId="72057602921267200">
<owner-list>
<owner id="processd734c8" mode="X"/>
</owner-list>
<waiter-list>
<waiter id="processa94e3948" mode="RangeS-S" requestType="wait"/>
</waiter-list>
</keylock>
<objectlock lockPartition="0" objid="1179111937" subresource="FULL" dbid="7" objectname="TravelWisconsinWeb.dbo.Versions" id="lock188faa980" mode="S" associatedObjectId="1179111937">
<owner-list>
<owner id="processa94e3948" mode="S"/>
</owner-list>
<waiter-list>
<waiter id="processd734c8" mode="IX" requestType="convert"/>
</waiter-list>
</objectlock>
</resource-list>
</deadlock>
</deadlock-list>
BlogArticleGetBlogArticlesByFiltersWithPagingCount SP:
DECLARE @mostRecentlyPublished TABLE (Id int , BaseContentId int)
;WITH CategoryTree AS
(
SELECT Id, ParentId
FROM Categories
WHERE Id = @Category
UNION ALL
SELECT c.Id, c.ParentId
FROM Categories c
INNER JOIN CategoryTree as ct ON ct.Id = c.ParentId
)
INSERT INTO @mostRecentlyPublished
SELECT DISTINCT pba.Id as Id, pba.BaseContentId
FROM BlogArticles i
INNER JOIN PublishedBlogArticleIds pba ON pba.Id = i.Id
inner join ContentPages cp on cp.Id = i.ContentPageId
LEFT JOIN BlogArticle_Categories pc ON pc.BlogArticleId = i.Id
WHERE i.BlogArticleTypeId = @BlogArticleTypeId
ANd pba.SiteId = @SiteId
AND
(
(
@Category IS NULL
OR
(
pc.CategoryId IN (SELECT Id FROM CategoryTree)
)
)
AND
(
(@ArchiveDate IS NULL AND GETDATE() BETWEEN cp.PublishStartDate AND COALESCE (cp.PublishEndDate, CONVERT(DATETIME,'9999-12-31')))
OR
(
Year(cp.PublishStartDate) = YEAR(@ArchiveDate)
AND
MONTH(cp.PublishStartDate) = MONTH(@ArchiveDate)
AND
GETDATE() >= cp.PublishStartDate
)
)
)
declare @blogContentTypeId int = (select id from ContentTypes where Name = 'Blog' and SiteId = @SiteId);
declare @taggedBlogs table (id int);
insert into @taggedBlogs
select distinct v.BaseContentId
from Tags t
inner join ContentPages cp on cp.Id = t.ContentPageId
inner join BlogArticles ba on ba.ContentPageId = cp.Id and ba.BlogArticleTypeId = @BlogArticleTypeId
inner join Versions v on (v.ContentId = ba.Id or v.BaseContentId = ba.Id) and v.ContentTypeId = @blogContentTypeId
where t.Tag in (SELECT Data FROM dbo.Split(@Tags, ','));
SELECT count(p.id)
FROM BlogArticles p
INNER JOIN @taggedBlogs d ON d.Id = p.Id
INNER JOIN ContentPages cp on cp.Id = p.ContentPageId
INNER JOIN TreeNodes tr On tr.Id = cp.TreeNodeId
LEFT JOIN BlogArticle_Categories bc on bc.BlogArticleId = d.Id AND bc.IsPrimary = 1
LEFT JOIN Categories c ON bc.CategoryId = c.Id
LEFT JOIN MediaFiles mf ON mf.Id = p.MediaFileId
LEFT JOIN Users u on u.Id = p.AuthorId
WHERE cp.SiteId = @SiteId;
Version Insert Query:
insert into Versions
(BaseContentId, ContentId, StatusId, SiteId, IsPublished, ModifiedByUserId, ModifiedDate, ModifiedMessage, ContentTypeId)
values (@BaseContentId, @ContentId, @StatusId, @SiteId, @IsPublished, @ModifiedByUserId, GETDATE(), @ModifiedMessage, @ContentTypeId);
SET @Id = @@IDENTITY;
IF @IsPublished = 1
BEGIN
-- make sure all basecontent is not marked as published except the newly published one
UPDATE [Versions] SET [IsPublished] = 0
WHERE [BaseContentId] = @BaseContentId AND [ContentTypeId] = @ContentTypeId AND [SiteId] = @SiteId AND [Id] != @Id
END
ELSE
BEGIN
DECLARE @StatusName nvarchar(255)
SELECT @StatusName = [CodeName] FROM [VersionStatuses] WHERE [Id] = @StatusId
IF (@StatusName = 'Archived' or @StatusName='Deleted')
BEGIN
-- make sure all basecontent is not marked as published
UPDATE [Versions] SET [IsPublished] = 0
WHERE [BaseContentId] = @BaseContentId AND [ContentTypeId] = @ContentTypeId AND [SiteId] = @SiteId
END
END
The call the VersionInsert Stored Procedure is one of the last calls from the TransactionScope. The TransactionScope starts when a web form is posted to the server. During the one post, all in the same TransactionScope, there could 10 – 20 different calls into the database including selects and inserts to and from several different tables.
Best Answer
BlogArticleGetBlogArticlesByFiltersWithPagingCount
selects fromContentPages
three times, and fromVersions
in the middle query.VersionInsert
ran first, though, and for some reason already has a lock onContentPages
- it's impossible to tell if that's code from the procedure but that you haven't shown, code from a trigger somewhere, or code called from within the same TransactionScope() out in the app code. In any case, one transaction is trying to lockContentPages
and thenVersions
, and the other is trying to lockVersions
and thenContentPages
. This is like chasing your tail - it's not possible to resolve this without one of them giving up their attempt; in this case the longer procedure name about the BlogArticles is the one that has been chosen as a victim.Here's how SQL Sentry software visualizes the deadlock graph (click to enlarge):
Since I can't see all of the code (and don't want to!), I can't give you specific advice on how to resolve this, but generally - and as I suggested in a comment - you don't want to control a large number of individual operations or procedure calls from a large, over-encompassing transaction. Transactions should be small, fast interactions that don't rely on affecting a large number of tables - this is bad for logging, bad for locking, and terrible for concurrency. You say:
But I really doubt that this is actually necessary. You should validate whether all of those calls really need to succeed or fail as a single unit, and consolidate code where you can. The CTE and a bunch of table variables in the BlogArticles procedure, for example, look like candidates for turning 3+ queries into one.