Deadlock'lar: Deadlock Graph'ı Okumak, Yaygın Pattern'ler ve .NET'te Retry
SQL Server deadlock'ta geri alması en ucuz transaction'ı kurban seçer ve 1205 döner; çözüm graph'ı okumak, erişim sırasını sabitlemek, covering index ya da RCSI ile reader-writer döngüsünü kırmak ve bütün transaction'ı iş kuralını yeniden kontrol ederek tekrar denemektir. Ters sıra, HOLDLOCK'lu upsert ve key lookup deadlock'ları, DEADLOCK_PRIORITY, EF Core'un execution strategy'si ve Polly ile retry'ın iptali ezdiği gerçek bir örnek.
Sürüm notu: Bu yazıdaki SQL (Structured Query Language) örnekleri SQL Server 2025 (17.0.5035.5, Cumulative Update 10) üzerinde, compatibility level 170 ile çalıştırılmıştır; .NET örnekleri .NET 10, Microsoft.Data.SqlClient 7.1.1, Entity Framework Core (EF Core) 10.0.14 ve Polly.Core 8.9.1 ile derlenip denenmiştir.
Önce Tahmin Et: Hangisi Kurban Olur?
Bookstore'da iki iş aynı anda çalışıyor. Checkout önce kitabın stoğunu ayırıyor, sonra siparişi güncelliyor. İptal işlemi önce siparişi iptal ediyor, sonra kitabı stoğa geri koyuyor. Aynı sipariş, aynı kitap, ters sıra:
/* checkout: reserve stock, then touch the order */
BEGIN TRANSACTION;
UPDATE dbo.StockItems SET QuantityReserved += 1 WHERE BookId = 1;
WAITFOR DELAY '00:00:02';
UPDATE dbo.Orders SET Status = 'PaymentPending' WHERE OrderId = 999001;
COMMIT;
/* cancel: touch the order, then return the book to stock */
BEGIN TRANSACTION;
UPDATE dbo.Orders SET Status = 'Cancelled' WHERE OrderId = 999001;
WAITFOR DELAY '00:00:02';
UPDATE dbo.StockItems SET QuantityOnHand += 1 WHERE BookId = 1;
COMMIT;
İkisini yarım saniye arayla başlatıyoruz. Biri bitecek, biri hata alacak. Hangisi kurban olur: önce başlayan mı, sonra başlayan mı, yoksa SQL Server başka bir şeye mi bakar?
Msg 1205, Level 13, State 51, Server b8be943222fa, Line 5
Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
Kurban checkout oldu. Sıra değil, maliyet belirledi: SQL Server, geri alması en ucuz olan transaction'ı seçer ve bunu transaction log'a yazılan byte miktarıyla ölçer. Birazdan graph'ta checkout'un logused="240", iptalin logused="276" olduğunu göreceğiz. Bu yazıda deadlock graph'ı yakalamayı ve okumayı, Bookstore'da gerçekten yaşanan üç pattern'i, önleme yollarını ve .NET tarafında 1205'i güvenle yeniden denemeyi ele alıyoruz.
Deadlock, bir önceki yazıdaki blocking zincirinin döngüye dönüşmüş hâlidir: A, B'nin tuttuğunu bekler; B de A'nın tuttuğunu. Kimse kendi başına çıkamaz. SQL Server'ın lock monitor'ü varsayılan olarak beş saniyede bir bu döngüleri arar, bulduğunda birini kurban seçer, transaction'ını geri alır ve 1205 hatasını o session'a döndürür.
Deadlock Graph'ı Yakalamak
Her deadlock'un bir raporu vardır: xml_deadlock_report event'i. SQL Server'ın her kurulumda açık gelen system_health Extended Events session'ı bu event'i zaten topluyor; bir production sunucusunda ilk bakacağın yer orası. Bu laboratuvar başka işlerin de çalıştığı ortak bir sunucuda olduğu için kendi session'ımızı açtık:
CREATE EVENT SESSION sqls_12_deadlocks ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.ring_buffer
WITH (MAX_DURATION = 2 HOURS);
GO
ALTER EVENT SESSION sqls_12_deadlocks ON SERVER STATE = START;
İlk denememizde event'e WHERE database_name = N'sqls_12' filtresi koymuştuk ve hiçbir event gelmedi. Sebebi şu: xml_deadlock_report'un kendi alanları yalnız xml_report'tan ibaret. Filtre, o anki session'ın veritabanına bakan global predicate'e dönüşüyor; event ise lock monitor'ün sistem bağlamında üretiliyor. Filtreyi okurken uygulamak gerekiyor:
SELECT x.e.query('(data[@name="xml_report"]/value/deadlock)[1]') AS deadlock_graph
FROM sys.dm_xe_session_targets AS t
JOIN sys.dm_xe_sessions AS s ON s.address = t.event_session_address
CROSS APPLY (SELECT CAST(t.target_data AS xml)) AS d(x)
CROSS APPLY d.x.nodes('/RingBufferTarget/event[@name="xml_deadlock_report"]') AS x(e)
WHERE s.name = 'sqls_12_deadlocks'
AND x.e.exist('data/value/deadlock/process-list/process[@currentdbname="sqls_12"]') = 1;
system_health için de aynı sorgu, session adı ve target değişerek çalışır. Ring buffer bellekte durur ve sınırlıdır; aşağıdaki key lookup denemesinde 1.955 deadlock ürettik ve buffer'da yalnız son 285'i kaldı. Kalıcı kayıt için event_file target'ı kullan.
Graph'ı Okumak
Ham XML (Extensible Markup Language) uzun; çoğu, Linux'ta bile Windows modül adlarıyla dolu bir stackFrames bölümü. İşe yarayan kısımları ayıklayınca açılıştaki deadlock şöyle görünüyor:
<victim-list><victimProcess id="processd981b8088"/></victim-list>
<process id="processd981b8088" spid="54" lockMode="X"
waitresource="KEY: 55:72057594047430656 (94e983785ab1)"
isolationlevel="read committed (2)" logused="240">
inputbuf: UPDATE dbo.Orders SET Status = 'PaymentPending' WHERE OrderId = 999001;
<process id="processdd48d6c58" spid="60" lockMode="X"
waitresource="KEY: 55:72057594047561728 (8194443284a0)"
isolationlevel="read committed (2)" logused="276">
inputbuf: UPDATE dbo.StockItems SET QuantityOnHand += 1 WHERE BookId = 1;
<resource-list>
<keylock objectname="sqls_12.dbo.Orders" indexname="PK_Orders" mode="X">
<owner id="processdd48d6c58" mode="X"/>
<waiter id="processd981b8088" mode="X" requestType="wait"/>
<keylock objectname="sqls_12.dbo.StockItems" indexname="PK_StockItems" mode="X">
<owner id="processd981b8088" mode="X"/>
<waiter id="processdd48d6c58" mode="X" requestType="wait"/>
Okuma sırası her zaman aynı. Önce resource-list: her kaynak için kim sahip (owner), kim bekliyor (waiter). Burada iki kaynak var: PK_Orders'taki sipariş key'i iptalin elinde, PK_StockItems'taki stok key'i checkout'un elinde ve her biri diğerininkini bekliyor. Döngü bu. Sonra process-list: her process'in o anda hangi statement'ta beklediği (inputbuf ve executionStack), isolation level'ı ve log kullanımı. Son olarak victim-list: hangisinin feda edildiği.
Graph'ta göremeyeceğin şey, lock'ların hangi sırayla alındığıdır. Onu kodu okuyarak çıkarırsın: checkout'un elinde stok var ve siparişi istiyor; iptalin elinde sipariş var ve stoğu istiyor. Bir de dikkat: inputbuf bütün batch'i gösterir (biz burada kısalttık), executionStack ise beklenen satırı. Stored procedure'lerde procname ve line doğrudan satıra götürür.
Yaygın Pattern'ler
1. Ters erişim sırası
Açılıştaki örnek. İki iş aynı kaynaklara farklı sırayla dokunuyor. Gerçek kodda bu sıra çoğu zaman açıkça görünmez: checkout service'i önce InventoryService.ReserveAsync'i, sonra OrderService.UpdateStatusAsync'i çağırıyordur, iptal tarafında başka bir geliştirici tersini yazmıştır. Çözüm basit ama disiplin ister: aynı transaction'da birden fazla tabloya yazan her kod yolu aynı sırayı izlemeli. Bookstore için kural "önce Orders, sonra StockItems" olabilir.
2. Önce kontrol et, sonra ekle
Bir önceki yazıda upsert yarışını UPDLOCK, SERIALIZABLE ile çözmüştük. Aynı kodu yalnız HOLDLOCK ile yazarsan yarış deadlock'a dönüşür:
DECLARE @d date = '2026-11-08', @b int = 42;
BEGIN TRANSACTION;
IF NOT EXISTS (SELECT 1 FROM dbo.DailyBookSales WITH (HOLDLOCK)
WHERE SalesDate = @d AND BookId = @b)
BEGIN
WAITFOR DELAY '00:00:02';
INSERT dbo.DailyBookSales (SalesDate, BookId, Quantity) VALUES (@d, @b, 1);
END
ELSE
UPDATE dbo.DailyBookSales SET Quantity += 1 WHERE SalesDate = @d AND BookId = @b;
COMMIT;
<process id="processdaac7e868" spid="60" lockMode="RangeI-N"
waitresource="KEY: 55:72057594048348160 (ffffffffffff)" isolationlevel="read committed (2)">
<process id="processda5147438" spid="54" lockMode="RangeI-N"
waitresource="KEY: 55:72057594048348160 (ffffffffffff)" isolationlevel="read committed (2)">
<keylock objectname="sqls_12.dbo.DailyBookSales" indexname="PK_DailyBookSales" mode="RangeS-S">
<owner-list>
<owner id="processda5147438" mode="RangeS-S"/>
<owner id="processda5147438" mode="RangeI-N" requestType="convert"/>
</owner-list>
<waiter-list><waiter id="processdaac7e868" mode="RangeI-N" requestType="convert"/></waiter-list>
Tablo boş olduğu için iki session da aynı kaynağı, son key'den sonsuza kadarki aralığı (ffffffffffff) RangeS-S ile kilitliyor. RangeS-S kendisiyle uyumlu, ikisi de alıyor. INSERT zamanı gelince ikisi de lock'unu RangeI-N'e (insert) çevirmek istiyor ve her biri diğerinin RangeS-S'ini bekliyor. Graph'taki requestType="convert" bunun conversion deadlock olduğunu söylüyor. isolationlevel hâlâ read committed görünüyor, çünkü hint statement seviyesinde değil tablo seviyesinde. Çözüm, okuma lock'unu baştan U almak: UPDLOCK'lu versiyonda ikinci session deadlock yerine bekler.
3. Key lookup deadlock'u: okuyucu ve yazıcı
En şaşırtıcı olanı, çünkü taraflardan biri yalnız SELECT yapıyor. Okuyucu bir müşterinin siparişlerinin durumunu okuyor; yazıcı bir siparişi başka müşteriye taşıyor:
/* reader */
SELECT @s = Status FROM dbo.Orders WHERE CustomerId = 15018;
/* writer */
UPDATE dbo.Orders
SET CustomerId = CASE CustomerId WHEN 15018 THEN 15709 ELSE 15018 END
WHERE OrderId = 250163;
Okuyucunun planı nonclustered index seek ve ardından key lookup:
|--Nested Loops(Inner Join, OUTER REFERENCES:([sqls_12].[dbo].[Orders].[OrderId]))
|--Index Seek(OBJECT:([sqls_12].[dbo].[Orders].[IX_Orders_CustomerId]), SEEK:([sqls_12].[dbo].[Orders].[CustomerId]=(15018)) ORDERED FORWARD)
|--Clustered Index Seek(OBJECT:([sqls_12].[dbo].[Orders].[PK_Orders]), SEEK:([sqls_12].[dbo].[Orders].[OrderId]=[sqls_12].[dbo].[Orders].[OrderId]) LOOKUP ORDERED FORWARD)
İki sorguyu iki session'da 20.000'er kez döngüyle çalıştırdık. Okuyucu 1.955 kez kurban oldu, yazıcı hiç. Graph:
<process id="processdaac7e868" spid="60" lockMode="S"
waitresource="KEY: 55:72057594047430656 (21a883cfb4e3)" logused="0">
<process id="processda5147438" spid="54" lockMode="X"
waitresource="KEY: 55:72057594048020480 (cb19ef703de5)" logused="220">
<keylock objectname="sqls_12.dbo.Orders" indexname="PK_Orders" mode="X">
<owner id="processda5147438" mode="X"/>
<waiter id="processdaac7e868" mode="S" requestType="wait"/>
<keylock objectname="sqls_12.dbo.Orders" indexname="IX_Orders_CustomerId" mode="S">
<owner id="processdaac7e868" mode="S"/>
<waiter id="processda5147438" mode="X" requestType="wait"/>
Aynı tablo, iki farklı index. Okuyucu index'i soldan sağa okuyor: önce IX_Orders_CustomerId'deki key'e S, sonra lookup için PK_Orders'taki satıra S. Yazıcı ise ters yönde: önce clustered index'teki satıra X, sonra CustomerId değiştiği için nonclustered index'teki key'e X. Yine ters erişim sırası, ama bu sefer tek tablonun içinde ve kimse sırayı kodda yazmadı. Okuyucunun log kullanımı 0 olduğu için her seferinde o kurban seçiliyor.
4. Cascade ve foreign key kontrolleri
ON DELETE CASCADE'li bir foreign key, parent satırı silen statement'ın child tablolarda da lock almasına yol açar ve bu lock'lar, aynı child satırlara başka sırayla gelen işlerle döngü kurabilir. Foreign key'lerin index'siz olması da aynı sonucu doğurur: parent'tan silme, child tabloyu taramak zorunda kalır ve çok daha geniş bir alanda lock alır. Bookstore'un şemasında cascade yok ve tüm foreign key kolonlarının index'i var; bu iki kuralı kendi şemanda da kontrol et.
Önleme: Sıra, Index, Kısa Transaction ve RCSI
Deadlock'u sıfıra indirmek her zaman mümkün değildir; amaç sıklığı düşürmek ve kalanları güvenle yeniden denemek. Key lookup deadlock'u üzerinde iki önlemi ölçtük; her koşuda okuyucu ve yazıcı 3.000'er iterasyon çalıştı:
/* 1: covering index */
CREATE INDEX IX_Orders_CustomerId ON dbo.Orders (CustomerId) INCLUDE (Status) WITH (DROP_EXISTING = ON);
/* 2: RCSI, eski index ile */
ALTER DATABASE sqls_12 SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;
variant reader_deadlocks writer_deadlocks
baseline (IX_Orders_CustomerId) 261 0
covering index INCLUDE (Status) 0 0
RCSI on, original index 0 0
Covering index, okuyucunun ikinci kaynağa hiç ihtiyaç duymamasını sağlıyor: Status artık nonclustered index'in leaf seviyesinde, lookup yok. Tek bir kaynak kilitleyen okuyucu döngünün parçası olamaz. RCSI (Read Committed Snapshot Isolation) ise okuyucunun lock almasını tamamen kaldırıyor: okuyucu satırın son commit edilmiş versiyonunu version store'dan okuyor. Reader-writer deadlock'larının hepsi bu tek ayarla kaybolur; writer-writer deadlock'ları (pattern 1 ve 2) kalır.
Kısa transaction'ın etkisi daha az görünür ama en geneldir: lock ne kadar kısa tutulursa döngü kurulma ihtimali o kadar düşer. Deadlock'u laboratuvarda üretebilmek için araya WAITFOR koymak zorunda kaldık; production'da o boşluğu transaction içindeki bir HTTP (Hypertext Transfer Protocol) çağrısı, gereksiz bir SELECT ya da kullanıcı onayı bekleyen bir ekran doldurur.
Son araç DEADLOCK_PRIORITY. Kurbanı maliyet yerine önceliğe göre seçtirir: LOW, NORMAL, HIGH ya da -10 ile 10 arası bir sayı. Gece çalışan bir rapor ya da yeniden denenmesi kolay bir arka plan işi için uygundur. Açılıştaki örnekte iptal session'ına SET DEADLOCK_PRIORITY LOW; ekleyince:
Msg 1205, Level 13, State 51, Server b8be943222fa, Line 6
Transaction (Process ID 63) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
Bu sefer hatayı iptal aldı; log'u daha fazla kullanmasına rağmen önceliği düşük olduğu için feda edildi ve checkout iki UPDATE'ini de tamamladı. Öncelik, maliyetten önce gelir; maliyet yalnız öncelikler eşitse belirleyicidir.
.NET'te 1205'i Yeniden Denemek
Kurban seçilen transaction tamamen geri alınmıştır; veritabanı açısından tekrar çalıştırmak güvenlidir. Hata mesajı da bunu söylüyor: "Rerun the transaction." Kritik kelime transaction: statement'ı değil, bütün transaction'ı baştan çalıştırmalısın. Açılıştaki deadlock'u iki Task ile .NET'ten üretip SqlClient'ın exception'ına bakalım ve EF Core'un bu hatayı geçici (transient) sayıp saymadığını soralım:
SqlException victim = await lab.RunWithoutRetryAsync();
Console.WriteLine($"Number={victim.Number} Class={victim.Class} State={victim.State}");
#pragma warning disable EF1001
Console.WriteLine($"EF Core transient: {Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerTransientExceptionDetector.ShouldRetryOn(victim)}");
#pragma warning restore EF1001
checkout: SqlException 1205: Transaction (Process ID 63) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
cancel: committed
Number=1205 Class=13 State=51
EF Core transient: True
EF Core 10'un SqlServerTransientExceptionDetector'ı 1205'i transient sayıyor; GitHub'daki kaynakta liste 1205'in yanında 1204 ve lock timeout hatası 1222'yi de içeriyor. Bu sınıf internal bir API (uyarı EF1001), burada yalnız doğrulamak için çağırdık. Yani EnableRetryOnFailure() açıkken EF Core deadlock'u kendisi yeniden dener. Ama bir şartla:
await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync();
Order order = await db.Orders.SingleAsync(o => o.OrderId == 999001);
EF Core: The configured execution strategy 'SqlServerRetryingExecutionStrategy' does not support user-initiated transactions. Use the execution strategy returned by 'DbContext.Database.CreateExecutionStrategy()' to execute all the operations in the transaction as a retriable unit.
Kendi açtığın transaction'ı EF Core yarıda yakalayıp tekrar edemez, çünkü transaction'ın başından beri ne yaptığını bilmez. Çözüm, transaction'ın tamamını strategy'ye vermek:
IExecutionStrategy strategy = db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync();
Order order = await db.Orders.SingleAsync(o => o.OrderId == 999001);
order.Status = "PaymentPending";
await db.SaveChangesAsync();
await tx.CommitAsync();
});
EF Core serisinin transaction yazısında bu kalıbı görmüştük; burada neden zorunlu olduğunu gördük. Dapper ya da düz ActiveX Data Objects for .NET (ADO.NET) kullanıyorsan aynı işi Polly ile kurarsın. Polly'nin retry strategy'si yalnız 1205'i yakalıyor, aralara exponential backoff ve jitter koyuyor:
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<SqlException>(ex => ex.Number == 1205),
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
Delay = TimeSpan.FromMilliseconds(200),
OnRetry = args =>
{
Console.WriteLine($"retry #{args.AttemptNumber + 1}: deadlock victim, rerunning the transaction");
return default;
}
})
.Build();
Task checkout = pipeline.ExecuteAsync(async ct =>
await RunTransactionAsync("checkout", ReserveStock, markPaymentPending, meeting, ct)).AsTask();
RunTransactionAsync her denemede yeni bir connection açıyor, transaction'ı baştan başlatıyor ve iki UPDATE'i çalıştırıyor. Çıktı:
checkout: SqlException 1205: Transaction (Process ID 64) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
cancel: committed
retry #1: deadlock victim, rerunning the transaction
checkout: committed
final: Status=PaymentPending OnHand=37 Reserved=1
Retry çalıştı ve teknik olarak her şey yolunda. Ama son satıra bak: sipariş iptal edilmişti, kitap stoğa dönmüştü ve retry edilen checkout siparişi PaymentPending'e çekip stoğu yeniden ayırdı. İptal kayboldu. Retry'ın idempotency şartı tam olarak bu: yeniden çalışan transaction, aradaki dünyanın değiştiğini varsaymalı ve iş kuralını yeniden kontrol etmeli. Checkout'un UPDATE'ine durum kontrolü ekleyince:
UPDATE dbo.Orders SET Status = 'PaymentPending' WHERE OrderId = 999001 AND Status = 'Pending';
IF @@ROWCOUNT = 0 THROW 50001, 'Order 999001 is no longer Pending.', 1;
checkout: SqlException 1205: Transaction (Process ID 63) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
retry #1: deadlock victim, rerunning the transaction
cancel: committed
checkout: SqlException 50001: Order 999001 is no longer Pending.
checkout: gave up, the order was cancelled meanwhile
final: Status=Cancelled OnHand=37 Reserved=0
Retry yine çalıştı, ama bu sefer iş kuralı kazandı: sipariş iptal, rezervasyon geri alındı. Aynı kural veritabanı dışındaki yan etkiler için daha da sıkıdır. Transaction içinde e-posta gönderdiysen ya da ödeme sağlayıcısına istek attıysan, retry onları da tekrarlar; bu yüzden onlar transaction'ın dışında, outbox gibi bir mekanizmayla yapılır.
SQL Server 2019/2022'de Farkı
Deadlock tespiti, kurban seçimi, DEADLOCK_PRIORITY ve xml_deadlock_report'un yapısı SQL Server 2019 ve 2022'de aynıdır; bu yazıdaki graph'lar o sürümlerde de böyle görünür. İki fark var. MAX_DURATION'lı time-bound Extended Events session'ı 2025 ile geldi; eski sürümlerde session'ı kendin durdurursun. SQL Server 2025'te optimized locking açıksa, writer-writer deadlock'larında kaynaklar keylock yerine xactlock elementiyle raporlanır; 2019/2022'de bu element yoktur. Bunu bir sonraki yazıda göreceğiz. EF Core'un transient hata listesi sunucu sürümüne bağlı değildir; 1205, 2019 ve 2022'ye bağlanırken de yeniden denenir.
Üretimde Dikkat
Deadlock sayısını izle ama mutlak sayıya değil eğilime bak. Günde birkaç deadlock, retry'ı olan bir sistemde sorun değildir; aynı graph'ın saatte yüzlerce kez tekrarlaması bir tasarım hatasıdır. Graph'ları resource-list'teki index adlarına ve inputbuf'taki statement'a göre gruplamak, hangi pattern'in baskın olduğunu birkaç dakikada gösterir.
Uygulama logunda 1205'i ayrı bir event olarak tut. Retry'dan sonra başarılı olan bir istek kullanıcıya hata göstermez ama veritabanında bir sorunun belirtisidir. Polly'nin OnRetry'ı ya da EF Core'un execution strategy log'u bunun yeri.
Retry sayısını sınırla ve backoff kullan. Aynı iki iş aynı anda tekrar başlarsa aynı deadlock'u tekrar yaşar; jitter, iki tarafın aynı anda geri dönmesini engeller. Üç denemeden sonra hâlâ deadlock alıyorsan retry değil düzeltme gerekiyordur.
Uzun transaction'lar kurban olmaz, kurban ettirir. SQL Server log'u en az kullananı seçtiği için büyük bir batch işi neredeyse hiç kurban olmaz; ona çarpan küçük, kullanıcıya dönük istekler kurban olur. Kullanıcı "kaydet'e bastım, hata aldım" diyorsa arka planda çalışan büyük işlere bak.
Sık Düşülen Tuzaklar
Statement'ı yeniden denemek. 1205 geldiğinde transaction çoktan geri alınmıştır. Yalnız son statement'ı tekrar çalıştırırsan, ilk statement'ın değişikliği kaybolmuş olur ve veri tutarsız kalır. Yeniden deneme birimi her zaman transaction'dır.
NOLOCK ile deadlock'u "çözmek". Reader-writer deadlock'unu kaybettirir ama bedeli tutarsız okumadır; aynı sonucu RCSI ile, doğru veri okuyarak alırsın.
Deadlock'u yalnız uygulama loglarından araştırmak. Uygulama yalnız kurbanı görür; diğer tarafın kim olduğu, hangi index'te beklediği yalnız graph'ta vardır. system_health'i okumayı öğren.
TransactionScope ile retry'ı karıştırmak. Ambient transaction'ın içinde açılan connection'lar, retry sırasında beklemediğin şekilde aynı transaction'a dahil olabilir ve birden fazla connection açılırsa distributed transaction'a yükselebilir. Retry sınırını açıkça yönetilen tek bir transaction'ın etrafına çiz.
Küçük Bir Deneme
★☆☆ Açılıştaki iki script'i iptal tarafı önce başlayacak şekilde çalıştır. Kurban değişiyor mu? logused değerlerine bakarak açıkla.
★★☆ Pattern 2'deki upsert'i MERGE ile yaz, hint koymadan iki session'da çalıştır. Deadlock mı, primary key ihlali mi, yoksa doğru sonuç mu alıyorsun? Sonra WITH (HOLDLOCK) ekleyip tekrar dene.
★★★ Dapper ile PlaceOrderAsync yaz: sipariş satırı, sipariş kalemleri ve stok rezervasyonu tek transaction'da. Polly ile 1205 retry'ı ekle ve retry'ın aynı siparişi iki kez oluşturmadığını bir idempotency key (örneğin istemcinin gönderdiği OrderNumber ve onun unique index'i) ile garanti et.
Klasik locking'de kaynaklar transaction boyunca kilitli kalıyor; deadlock da blocking de bundan doğuyor. SQL Server 2025 bu modeli değiştiren bir seçenekle geldi. Bir sonraki yazıda optimized locking'i açıyor, TID (Transaction ID) locking ve lock after qualification ile aynı UPDATE'in kaç lock tuttuğunu yeniden ölçüyor ve sonucun nerede değişebildiğini görüyoruz.