82 puan yazan GN⁺ 2026-05-30 | 6 yorum | WhatsApp'ta paylaş
  • Envanter rezervasyon sistemi, ödeme işleme sırasında aynı ürünün iki kez satılmasını önleyen kritik bir altyapı ve Shopify bunu yıllardır Redis tabanlı olarak işletiyordu
  • MySQL 8'in SKIP LOCKED özelliğinden yararlanarak, öğe başına miktar sütunu yerine satış birimi başına 1 satır yapısıyla sistemi yeniden tasarladı ve Redis olmadan da yüksek performans elde etti
  • Bileşik birincil anahtar, READ COMMITTED yalıtım seviyesi, tutarlı kilitleme sırası, UNION ALL ile toplu işleme gibi MySQL optimizasyon tekniklerini birleştirerek kilit çekişmesi ve deadlock sorunlarını giderdi
  • Gerçek darboğazın rezervasyon sorguları değil bağlantı işgali olduğu ortaya çıktı; checkout yolunun tamamı ölçümlenerek DB okumalarında %50, transaction sayısında %33 azalma sağlandı
  • 2025 Black Friday zirvesinde dakikada 5,1 milyon dolar gelir işlenirken writer CPU %50'nin, reader CPU ise %16'nın altında tutuldu ve hedef throughput aşıldı

Arka plan: Oversell önleme sisteminin gereksinimleri

  • Checkout tamamlandığında stokta gerçekten ürün kaldığını garanti eden bir Oversell Protection sistemine ihtiyaç var
    • Reserve: ödeme başlarken ilgili ürünü birkaç dakika boyunca geçici olarak kilitler
    • Claim: ödeme tamamlandığında stok defterinden miktarı kalıcı olarak düşer
  • Her iki yönde de hataya tolerans yok
    • Hata olursa aynı ürünü iki kişi satın alabilir ya da stok varken ürün tükendi sayılarak gelir kaybı oluşabilir
  • Ölçek gereksinimi: Shopify, ABD e-ticaretinin %14'ünden fazlasını taşıyor ve 2025 Black Friday'de yıllık bazda %11 artışla dakikada 5,1 milyon dolar gelir kaydetti
  • Çoklu konum envanteri (Multi-location inventory), ACID garantileri, yüksek performanslı throughput ve doğruluğun öncelikli olması temel gereksinimlerdi

Mevcut Redis modelinin sınırları

  • Redis'te her öğe bir miktar anahtarına sahipti; rezervasyon DECR, serbest bırakma ise INCR ile yapılıyordu
  • Temel sorun: rezervasyon verisi (Redis) ile stok defteri (MySQL) farklı sistemlerdeydi
    • Claim aşamasında MySQL güncellemesi ile Redis temizliğini tek bir atomik transaction içinde birleştirmek mümkün değildi
    • Çalışma sırasına bağlı olarak oversell (ürün satıldı ama defterden düşülmedi) veya undersell (defterden düşüldü ama hâlâ rezerve görünüyor) oluşabiliyordu
  • Çoklu konum stok farkındalığı yoktu ve ayrı bir Redis kümesini işletmenin maliyeti vardı

Temel çözüm: SKIP LOCKED tabanlı MySQL yeniden tasarımı

Temel yapı: birim başına 1 satır (One Row Per Unit)

  • Öğe başına miktar sütunu yerine satılabilir her birim için 1 satır yapısı benimsendi
    • Örneğin stokta 10 adet olan bir ürün → 10 satır; 3 adet rezervasyon için tek bir transaction içinde 3 satır seçilip taşınıyor
  • Rezervasyon ile stok defteri aynı MySQL veritabanında tutularak reserve ve claim işlemleri ACID transaction olarak yürütüldü; böylece Redis'teki hata sınıfları ortadan kaldırıldı
  • SKIP LOCKED: başka bir transaction'ın kilitlediği satırları atlayıp kullanılabilir satırları hemen döndürür → aynı satır üzerinde bekleme olmadan çekişmeyi azaltır

Havuz boyutu sınırı: konum başına en fazla 1.000 satır

  • Öğe/konum kombinasyonu başına kullanılabilir satırlar en fazla 1.000 adet ile sınırlandı; böylece tablo boyutu ve tarama performansı kontrol altında tutuldu
    • Örnek: 50.000 stok × 10 konum = 500.000 satırlık durumun önüne geçildi
  • Havuz tükendiğinde inline replenishment tetikleniyor; aynı anda çok sayıda transaction'ın satır eklemesini önlemek için yalnızca tek bir transaction'ın takviye yapmasına izin veriliyor ve böylece thundering herd engelleniyor
  • Havuz tamamen boşalsa bile gecikme yalnızca ilgili rezervasyonda yaşanıyor; stok gerçekten varsa müşterinin ürünü tükendi görmesi söz konusu olmuyor

Dört temel teknik karar

1. Bileşik birincil anahtarla kilit sayısını azaltmak

  • İlk prototipte auto-increment ID birincil anahtar olarak kullanıldığında, InnoDB hem ikincil indeksi hem de clustered index'i kilitlediği için rezervasyon başına 2 satır kilidi oluşuyordu
  • shop_id, inventory_item_id, inventory_group_id, id bileşenlerinden oluşan bileşik birincil anahtar uygulandı → filtreleme sütunları birincil anahtarın içinde olduğu için kilit sayısı 1'e düştü
  • Saniyede binlerce rezervasyonun olduğu ortamda indeks ve birincil anahtar tasarımı, kilit sayısını ve throughput'u doğrudan etkiliyor

2. READ COMMITTED ile gap lock'ları kaldırmak

  • Boş tabloda SELECT ... FOR UPDATE SKIP LOCKED çalıştırıldığında gap lock'lar (supremum dahil) oluşuyor, bu da replenishment transaction'ının INSERT işlemini engelliyor ve deadlock'a yol açıyordu
  • Yalıtım seviyesi MySQL varsayılanı olan REPEATABLE READ yerine READ COMMITTED olarak değiştirildi → gap lock davranışı değişti ve replenishment transaction'ı normal şekilde ilerledi
  • Bu, ilgili kod tabanında ilk kez varsayılan dışı yalıtım seviyesi kullanımıydı; bu yüzden transaction bazında yalıtım seviyesi ayarlamak için küçük bir framework desteği gerekti

3. Tutarlı kilit sırasıyla deadlock önlemek

  • Reserve ve claim iki tabloya farklı sırayla eriştiği için deadlock oluşuyordu
    • reserve: reserved_quantities INSERTreservation_units DELETE
    • claim: reserved_quantities DELETE
  • Çözüm: reserve işleminin her zaman önce units tablosunda DELETE, sonra reserved_quantities üzerinde INSERT yapacak şekilde sıra standartlaştırıldı → circular wait ortadan kaldırıldı

4. UNION ALL ile toplu işleme sayesinde round-trip azaltmak

  • Sepette birden fazla line item olduğunda rezervasyon sorguları UNION ALL ile tek bir round-trip içinde topluca işlendi
  • Toplam round-trip sayısının azalması, yük altında latency'yi iyileştirdi

Gerçek darboğaz: sorgular değil bağlantı işgali

Sorunun keşfedilme süreci

  • Üretim ortamında hedef throughput'un altında tavana ulaşıldı; P90 latency iyiydi, CPU sınırda değildi ve sorgular da optimize edilmişti
  • Yük testlerinde gözlenen belirtiler:
    • MySQL içinde thread queueing
    • Kuyruktaki işler çalışmaya başladığında CPU'nun aniden yükselmesi
    • ProxySQL katmanında MySQL backend bağlantılarının tükenmesi

Bağlantı görünürlüğü sağlamak

  • Uygulama katmanı: tüm SQL ifadelerine /* conn_tag:checkout_completion */ biçiminde iş sürecini tanımlayan yorumlar eklendi
  • ProxySQL katmanı: etiketleri ayrıştırıp çağıran tarafa göre bağlantı işgal süresi toplama yeteneği eklendi
  • Sonuç: hangi sürecin bağlantıyı ne kadar süre tuttuğu anında görülebilir hale geldi

Bulgular ve çözüm

  • Rezervasyon dışındaki checkout yolundaki başka kodlar, bağlantıları gerekenden uzun süre tutuyordu
    • Bunlar daha önce ilk sınıra çarpan bileşenler olmadığı için optimizasyon hedefi dışında kalmıştı
  • Checkout yolu sadeleştirilince primary DB okumaları %50, transaction sayısı %33 azaldı
  • Yıllar önce temkinli şekilde ayarlanıp tekrar gözden geçirilmeyen InnoDB thread concurrency ayarı güncellenerek ek darboğaz da kaldırıldı
  • İyileştirme sonrasında yüksek hacimli flash sale ölçümünde writer CPU %50'nin, reader CPU ise %16'nın altında kaldı

Geçiş yöntemi: Shadow Mode

  • Redis'ten MySQL'e bir anda geçmek yerine, Shadow Mode ile iki sistem paralel çalıştırıldı
    • Tüm rezervasyonlar aynı anda hem Redis'e hem MySQL'e yazıldı; source of truth olarak Redis korunmaya devam etti
    • Gerçek üretim trafiğinde MySQL'in doğruluğu ve performansı paralel olarak doğrulandı
  • Devam eden rezervasyonları taşımaya gerek kalmadan geçiş yapılabildi; çünkü iki sistem aynı anda aktifti
  • Source of truth MySQL'e geçirildikten sonra bile kill switch korundu ve çift yazma yolu sayesinde Redis her zaman güncel kaldı
  • Yaygınlaştırma, düşük trafikli pod'lardan en yüksek hacimli merchant'lara kadar pod bazında kademeli olarak yapıldı

Çıkarımlar

1. Eski kararları yeniden değerlendirin

  • 5 yıl önce mümkün olmayan bir MySQL yaklaşımı, SKIP LOCKED gibi yeni özelliklerle bugün uygulanabilir hale geldi
  • Thread limitleri gibi "pratik kural" ayarları, iş yükü ve donanım değiştiğinde yeniden gözden geçirilmeli
  • CPU düşükken queueing yaşanıyorsa, nedenini mutlaka derinlemesine araştırmak gerekir

2. Küçük başlayın ve gözlemleyin

  • Tam Rails framework'ü olmadan, küçük bir Ruby betiği ve MySQL ile minimal bir prototip kuruldu
  • İkinci bir terminalde kilit davranışını doğrudan gözlemlemek, teoriden daha fazla şey öğretti
  • Bağlantı işgali ölçüm kalıbı (uygulama katmanı etiketleri + proxy toplulaştırması) hem uygulanması kolay hem de hemen devreye alınabilir

6 yorum

 
hso2341 2026-05-30

Uzun zamandır gerçekten geliştirme yazısı gibi bir yazı paylaşılıyor.

 
redmi 29 일 전

Ben de uzun zamandır böyle bir yazı görmemiştim, hissi farklı geliyor.
Sanırım yapay zekadan fazlasıyla yorulmuşuz.

 
malcomjj 2026-05-31

Katılıyorum haha, böyle yazıları görmek sevindirici hale geldi!

 
goathead 29 일 전

AI Slop tanıtım yazılarından bıkan tek kişi ben değilmişim demek ki haha

 
kaydash 2026-05-30

Alkışlar

 
click 2026-05-31

PostgreSQL ya da Oracle'da eskiden beri skip locked yok muydu? diye düşünürken de
Oracle'ın lisans maliyeti başlı başına yük oluyor,
PG'de ise MVCC implementasyonu nedeniyle update yükünün epey fazla olacağı öngörüldüğünden, bu özelliğin MySQL 8'e gelmesinden sonra devreye almalarının nedeni bu olabilir diye tahmin ediyorum