500 Sunucu Hatasının Kabusu
Smart Tech Devs’te B2B SaaS platformunuz büyüdükçe, veri bütünlüğünü sağlamak için Database Transactions’a güvenmeye başlayacaksınız. Bir kullanıcı fatura ödediğinde, cüzdan bakiyesini güncellemeniz ve faturayı ödenmiş olarak işaretlemeniz gerekiyor. Eğer biri başarısız olursa, her ikisi de geri alınmalı. Ancak yüksek eşzamanlılık altında, korkutucu bir günlük girdisi görmeye başlayabilirsiniz: PDOException: SQLSTATE[40P01]: Deadlock detected.
Deadlock, iki eşzamanlı işlemin birbirinden veritabanı kilidini bırakmasını beklediğinde meydana gelir. İşlem A, Satır 1’i kilitler ve Satır 2’yi gerektirirken, İşlem B, Satır 2’yi kilitler ve Satır 1’i gerektirir. İkisi de ilerleyemez. PostgreSQL, bu sonsuz duraklamayı algılar ve bir işlemi sert bir şekilde sonlandırır, bu da başarısız bir API isteği ve hayal kırıklığına uğramış bir kullanıcı ile sonuçlanır.
Strateji 1: Tutarlı Kilit Sırası
Deadlockların %99’unun temel nedeni, öngörülemeyen kilit sırasıdır. Tüm işlemler satırları tam olarak aynı sırayla kilitlerse, deadlock matematiksel olarak imkansızdır. Bir işlem sadece diğerinin arkasında sabırla bekleyecektir.
Kullanıcıların cüzdanlar arasında fon transferi yapabileceği bir senaryoyu düşünün. Eğer dikkatli olmazsak, eşzamanlı transferler çökecek.
// ❌ TEHLİKELİ PATTERN (Deadlock'a Yol Açar)
public function transferFunds($fromWalletId, $toWalletId, $amount)
{
DB::transaction(function () use ($fromWalletId, $toWalletId, $amount) {
// İşlem A Cüzdan 1'i kilitler, İşlem B Cüzdan 2'yi kilitler
$from = Wallet::lockForUpdate()->find($fromWalletId);
$to = Wallet::lockForUpdate()->find($toWalletId);
$from->decrement('balance', $amount);
$to->increment('balance', $amount);
});
}
Bunu düzeltmek için, satırları kilitleme için katı bir sıralama uygulamalıyız; örneğin, her zaman en düşük ID’ye sahip cüzdanı önce kilitlemek.
// ✅ KURUMSAL PATTERN (Tutarlı Sıralama)
public function transferFundsSafely($fromWalletId, $toWalletId, $amount)
{
// ID'ye dayalı katı sıralamayı belirleme
$firstId = min($fromWalletId, $toWalletId);
$secondId = max($fromWalletId, $toWalletId);
DB::transaction(function () use ($firstId, $secondId, $fromWalletId, $amount) {
// Her zaman tam aynı sırada kilitlenir
$firstWallet = Wallet::lockForUpdate()->find($firstId);
$secondWallet = Wallet::lockForUpdate()->find($secondId);
// İş mantığı için referansları yeniden atama
$from = ($firstWallet->id == $fromWalletId) ? $firstWallet : $secondWallet;
$to = ($firstWallet->id == $fromWalletId) ? $secondWallet : $firstWallet;
$from->balance -= $amount;
$from->save();
$to->balance += $amount;
$to->save();
});
}
Strateji 2: Laravel Retry Yardımcısı
Mükemmel bir sıralama ile bile, arka plan görevlerini içeren karmaşık sistemler, indeks güncellemeleri veya karmaşık cascade’ler nedeniyle geçici deadlock’lar yaşayabilir. Bu durumlar için Laravel, son derece şık bir güvenlik ağı sunmaktadır: transaction retry parametresi.
Sadece DB::transaction metoduna ikinci bir argüman geçerek, Laravel’e deadlock istisnasını otomatik olarak yakalamasını ve tüm işlemi belirli bir sayıda denemesi için talimat vermiş olursunuz.
public function processComplexInvoice(Invoice $invoice)
{
// '3' Laravel'e şunu söyler: Eğer bir deadlock oluşursa, bu tüm bloğu 3 kez deneyin
// ardından nihayet 500 hatasını kullanıcıya fırlatın.
DB::transaction(function () use ($invoice) {
$invoice->markAsPaid();
$invoice->tenant->updateUsageMetrics();
$invoice->affiliate->processCommission();
}, 3);
}
Sonuç
Deadlock’lar, PostgreSQL’ün başarısız olduğu anlamına gelmez; bu, platformunuzun büyük ölçekleri başarıyla yönettiğinin bir işaretidir. Tutarlı kilit sıralamasını nasıl uygulayacağınızı ve Laravel’in yerleşik transaction retry mekanizmalarını nasıl kullanacağınızı anlayarak, kırılgan, çökme eğiliminde olan bir arka ucu, eşzamanlı trafiği kusursuz bir şekilde absorbe eden yüksek dayanıklı bir sisteme dönüştürebilirsiniz.
Kaynak: Orijinal Makale


