# Cache stampede'i önlemek için singleflight doğru bir yaklaşım mı?

> singleflight'ı stale-while-revalidate ile birlikte kurun; Redis distributed lock'ı ancak pod başına rebuild'lerin Postgres'i yorduğunu ölçünce ekleyin.

- Soruldu: 2026-10-24
- Yanıtlandı: 2026-10-30
- Soran: Emir
- Etiketler: Go, Concurrency, Önbellek
- Kaynak: https://muhammetsafak.com/tr/sor-bakalim/cache-stampedei-onlemek-singleflight-dogru-yaklasim/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
**Soru:** Go ile yazdığım bir API'de sıcak bir Redis anahtarı expire olduğunda, yüzlerce eşzamanlı istek aynı anda Postgres'e gidip aynı değeri yeniden hesaplıyor; klasik cache stampede.

İstediğim şey, o anahtar boşaldığında değeri yalnızca bir isteğin yeniden kurması, gerisinin sonucu paylaşması. `singleflight` bunun için doğru araç gibi görünüyor ama üretimde tek başına yeterli mi, yoksa gözden kaçırdığım noktalar var mı?


## Kısa cevap

Evet, singleflight in-process stampede çökertmesi için doğru araçtır — ama yalnızca tek proses içinde dedupe eder.

## Bağlam

Çok sayıda pod'da hâlâ pod başına bir rebuild alırsınız; bu sizin için önemliyse yanına stale-while-revalidate ya da probabilistik erken expire ekleyin.

## Neden

1. **singleflight tam olarak ne yapar?** `golang.org/x/sync/singleflight`, [aynı anahtarla gelen eşzamanlı çağrıları](https://muhammetsafak.com/tr/blog/goda-eszamanlilik-goroutine-ve-channel-pratigi/) tek bir çalıştırmada birleştirir; gerisi bekler ve sonucu paylaşır. Bu tam olarak sizin "yalnızca biri yeniden kursun" ihtiyacınızdır — tek proses içinde.
   ```go
   var g singleflight.Group

   func (s *Service) Get(ctx context.Context, key string) (Value, error) {
       if v, ok := s.cache.Get(key); ok {
           return v, nil
       }
       v, err, _ := g.Do(key, func() (any, error) {
           val, err := s.loadFromDB(ctx, key) // yalnızca biri çalışır
           if err != nil {
               return nil, err
           }
           s.cache.Set(key, val, ttl)
           return val, nil
       })
       if err != nil {
           return Value{}, err
       }
       return v.(Value), nil
   }
   ```
2. **Pod başına uyarısı.** Yük dengeleyici arkasında N replica varsa, bir değil N'e kadar eşzamanlı rebuild alırsınız. Çoğu uygulamada N küçüktür (diyelim 10) ve 10 sorgu sorun değildir. Değilse, cross-process bir lock gerekir (TTL'li Redis `SETNX`): yalnızca bir pod kurar, gerisi bekler ya da bayat servis eder.
3. **Paylaşılan hata ve panic tuzağına dikkat.** singleflight sonucu OLDUĞU KADAR hatayı da tüm bekleyenlere paylaştırır — tek bir başarısızlık o partideki herkesi düşürür. Yavaş bir rebuild'in çağıranları sonsuza kadar bloklamaması için context'li `DoChan` kullanın; geçici bir hatanın cachelenmemesi için `Forget`'i düşünün.

## Ne yapmalı

1. **Rebuild sırasında bayat servis etmeyi düşünün.** En düşük latency'li desen stale-while-revalidate'tir: soft bir TTL tutun, bayat değeri anında dönün ve tek bir arka plan yenilemesini (singleflight korumalı) tetikleyin. Kullanıcı rebuild'i hiç beklemez.
2. **Probabilistik erken expire (XFetch)** senkronize expire'ı azaltır: her istek, TTL'den biraz önce yenileme yapıp yapmayacağına zar atar; böylece sürü oluşma olasılığı düşer, garantisi yoktur. singleflight'in yerine değil, tamamlayıcısıdır.
3. **DB tarafını unutmayın.** Tek bir rebuild sorgusu bile hızlı ve index'li olmalı. Stampede koruması yavaş bir sorguyu gizler; düzeltmez.

**Sonuç:** Ben olsam singleflight + stale-while-revalidate ile başlardım — bu ikili uygulamaların büyük çoğunluğunda stampede'i öldürür ve latency'yi düz tutar. Redis distributed lock'ı yalnızca pod başına rebuild'lerin Postgres'i gerçekten yorduğunu ölçtüğümde eklerdim. Kısacası: replica sayınızın gerektirdiği en basit katmana uzanın, daha fazlasına değil.

## İlgili Yazılar

- [Go'da eşzamanlılık: goroutine ve channel pratiği](https://muhammetsafak.com/tr/blog/goda-eszamanlilik-goroutine-ve-channel-pratigi/) — Blog
- [Kanal tampon boyutu seçimi backpressure sorunlarını gizler mi, nasıl karar vermeliyim?](https://muhammetsafak.com/tr/sor-bakalim/kanal-tampon-boyutu-backpressure-sorunlarini-gizler-karar-vermeliyim/) — Sor Bakalım
- [Birbirine bağımlı birden fazla işi, ilk hata hepsini iptal edecek şekilde errgroup ile mi yürütmeliyim?](https://muhammetsafak.com/tr/sor-bakalim/birbirine-bagimli-birden-fazla-isi-ilk-hata-hepsini-iptal-edecek-sekilde/) — Sor Bakalım
- [İç içe geçmiş servis çağrılarında context iptalini doğru şekilde nasıl yayarım?](https://muhammetsafak.com/tr/sor-bakalim/ic-ice-gecmis-servis-cagrilarinda-context-iptalini-dogru-sekilde-nasil-yayarim/) — Sor Bakalım
