Redis ile gerçek zamanlı durum yönetimi
Terapist müsaitliğinden sepet durumuna: iki üründe Redis kullanım desenlerimiz.
Her iki ürünümüzde de benzer bir problem tekrar tekrar karşımıza çıkıyor: kısa ömürlü ama çok hızlı değişen durumu yönetmek. PsyBank'ta bir terapistin şu anda müsait olup olmadığı, bir randevu slotunun başka biri tarafından tutulup tutulmadığı; MatbaaStore'da bir kullanıcının sepetinde ne olduğu, sınırlı bir kampanya ürününden kaç adet kaldığı. Bu verilerin ortak özelliği, çok sık okunup yazılmaları ve genelde kalıcı olmalarına gerek olmaması. Bunları asıl veritabanına yıkmak, hem gereksiz yük hem de kabul edilemez bir gecikme demekti.
Bu yazıda Redis'i iki üründe nasıl bir "durum katmanı" olarak kullandığımızı anlatacağız. Terapist müsaitliğinden randevu kilitlerine, sepet yönetiminden stok rezervasyonuna kadar somut desenleri ve neden PostgreSQL yerine Redis'i seçtiğimizi ele alacağız. Ayrıca sık atlanan bir konuya, Redis'in dayanıklılık modeline ve gerçek zamanlı bildirimlerde kullandığımız pub/sub ile keyspace bildirimlerine değineceğiz.
Redis'i veritabanı değil, durum katmanı olarak görmek
Redis'i çoğu ekip "hızlı bir önbellek" olarak tanır, ama gücü aslında veri yapılarında. Bir dize, bir sayaç, bir hash, bir sıralı küme — bunların her biri belirli bir durum problemini neredeyse hiç kod yazmadan çözer. Biz Redis'i verinin kalıcı evi olarak değil, hızlı değişen durumun yaşadığı bir katman olarak konumlandırdık. Gerçeğin kaynağı hâlâ PostgreSQL; ama "şu an ne oluyor?" sorusunun cevabı Redis'te duruyor.
Bu ayrımı netleştirmek önemliydi, çünkü Redis'e her şeyi yüklemek cazip ama tehlikeli. Kalıcı olması gereken, ilişkisel bütünlüğü önemli olan veri PostgreSQL'de kalır. Redis'e ise yalnızca üç özelliği taşıyan durumu koyarız: çok sık değişir, kısa ömürlüdür ve milisaniye düzeyinde okunması gerekir. Bu üç ölçütü bir kontrol listesi gibi kullanmak, mimariyi temiz tuttu.
Redis'e ne koyacağınıza karar verirken şu soruyu sorun: bu veri bir gün kaybolsa dünya durur mu? Cevap "hayır, yeniden hesaplanır" ise Redis'in evindedir. "Evet, geri gelmez" ise yeri PostgreSQL.
PsyBank: müsaitlik ve çift rezervasyon kilidi
Bir randevu platformunda en tehlikeli hata, aynı slotun iki kişiye birden satılmasıdır. İki danışan aynı anda aynı saati seçerse, sadece biri o randevuyu alabilmeli. Bunu PostgreSQL'de kilit ve işlemlerle çözmek mümkün ama yoğun anlarda yavaş ve rekabet altında kırılgan. Redis'in atomik "SET NX EX" komutu bu problemi tek satırda çözüyor: anahtar yoksa oluştur, varsa dokunma, ve belirli bir süre sonra kendiliğinden sil.
func (r *Rezervasyon) SlotKilitle(ctx context.Context, slotID, danisanID string) (bool, error) {
anahtar := "slot:kilit:" + slotID
// NX: sadece yoksa yaz. EX: 90 saniye sonra otomatik serbest bırak.
ok, err := r.redis.SetNX(ctx, anahtar, danisanID, 90*time.Second).Result()
if err != nil {
return false, err
}
// ok=false ise slot zaten başkasında; kullanıcıya nazikçe söyleriz
return ok, nil
}
Buradaki "EX 90 saniye" ayrıntısı kritik. Kullanıcı slotu seçip ödeme ekranına geçtiğinde ona kısa bir süre tanıyoruz; bu sürede başkası aynı slotu tutamıyor. Kullanıcı ödemeyi tamamlarsa randevu PostgreSQL'e kalıcı olarak yazılıyor ve kilit görevini bitiriyor. Tamamlamazsa süre dolunca kilit kendiliğinden kalkıyor ve slot yeniden müsait hâle geliyor. Otomatik süre dolması olmasa, terk edilmiş her rezervasyon slotu sonsuza dek bloke ederdi.
Terapistin o anki müsaitlik durumunu ise ayrı bir yapı olarak tutuyoruz. Bir terapist görüşmeye başladığında "meşgul" durumu Redis'e yazılıyor ve arayan danışanlar bunu anlık olarak görüyor. Bu durum kalıcı olması gereken bir veri değil; terapist çevrimdışı olduğunda anlamını yitiriyor. Tam da bu yüzden Redis'te, kısa bir yaşam süresiyle duruyor. Randevu takviminin kendisi PostgreSQL'de; "şu anki durum" ise Redis'te.
MatbaaStore: sepet ve stok rezervasyonu
MatbaaStore tarafında en yoğun durum sepet. Bir kullanıcı gezinirken sepetine ürün ekliyor, çıkarıyor, adet değiştiriyor; bunların hepsi kalıcı bir kayıt gerektirmeyen, ama anında güncellenmesi gereken işlemler. Sepeti kullanıcı başına bir hash olarak tutuyoruz: her alan bir ürün, değeri ise adet ve seçili varyant bilgisi. Hash'in güzelliği, tek bir alanı güncellemek için tüm sepeti okuyup yazmak zorunda kalmamamız.
# Sepete ürün ekle (varyant ve adet)
HSET sepet:kullanici:8421 urun:kartvizit-500 "kuşe-300gr:2"
# Kullanıcı 30 gün dokunmazsa sepet kendiliğinden boşalsın
EXPIRE sepet:kullanici:8421 2592000
# Tüm sepeti tek seferde oku
HGETALL sepet:kullanici:8421
Sepete koyduğumuz her dokunuşta süreyi tazeliyoruz; kullanıcı aktifken sepet yaşıyor, uzun süre kaybolduğunda kendiliğinden temizleniyor. Böylece terk edilmiş sepetlerin veritabanını şişirmesi diye bir sorunumuz olmuyor. Sipariş tamamlandığında sepet Redis'ten okunup siparişe dönüştürülüyor ve kalıcı olarak PostgreSQL'e yazılıyor; Redis'teki geçici hâli artık gereksiz.
Sınırlı stoklu kampanya ürünlerinde ise stok rezervasyonu için Redis'in atomik sayaçlarını kullanıyoruz. Bir kullanıcı kampanya ürününü sepete eklediğinde stoğu atomik olarak azaltıyoruz; sayaç sıfırın altına düşerse işlemi geri alıp "tükendi" diyoruz. Atomiklik burada her şey: iki kullanıcı aynı anda son ürünü almaya çalışsa bile, yalnızca biri başarılı olabiliyor çünkü azaltma işlemi bölünemez.
- Hash ile sepet: Kullanıcı başına tek anahtar, ürün başına tek alan. Kısmi güncelleme ucuz, tüm sepeti okumak tek çağrı.
- EXPIRE ile otomatik temizlik: Terk edilmiş durumların birikmesini kod yazmadan engeller; her etkileşimde süre tazelenir.
- Atomik sayaç ile stok: Rekabet altında bile çift satışı önler; azaltma bölünemez olduğu için son ürün yalnızca bir kişiye gider.
- Kalıcılık sınırı: Sipariş kesinleşince veri PostgreSQL'e taşınır; Redis yalnızca "henüz kesinleşmemiş" durumu tutar.
Gerçek zamanlılık ve dayanıklılık
Durumu hızlı tutmak yetmiyor; değişiklikleri anında yansıtmak da gerekiyor. Terapist "meşgul" olduğunda ya da bir slot tutulduğunda, o sayfayı açık tutan diğer kullanıcılar bunu gecikmeden görmeli. Sunucular arasında bu değişiklikleri yaymak için Redis pub/sub kullanıyoruz. Bir olay bir sunucuda gerçekleştiğinde ilgili kanala yayınlanıyor, diğer sunucular abone oldukları için haberdar oluyor. Böylece birden çok sunucu örneği tek bir tutarlı durumu paylaşıyor.
Bir diğer yararlı özellik keyspace bildirimleri. Bir anahtarın süresi dolduğunda Redis bunu bir olay olarak yayınlayabiliyor. Randevu kilidi süresi dolduğunda tetiklenen bu olayı dinleyerek, slotun yeniden müsait olduğunu ilgili yerlere anında bildiriyoruz. Süre dolmasını sürekli sorgulayarak yoklamak yerine, olay geldiğinde tepki vermek hem daha temiz hem de daha ölçeklenebilir bir yaklaşım.
Dayanıklılık konusunda ise gerçekçi olmak gerekiyor. Redis belleğinde çalışır; varsayılan olarak bir çökme, bellekteki durumu kaybettirebilir. Bunu iki şekilde ele aldık. Birincisi zihniyet: Redis'e koyduğumuz her şey ya yeniden hesaplanabilir ya da kaybı tolere edilebilir olacak şekilde tasarlandı. Kalıcı olması gereken hiçbir veri yalnızca Redis'te durmuyor. İkincisi yapılandırma: kalıcılığı açık tutuyor, düzenli anlık görüntü ve olay günlüğüyle yeniden başlatmada durumu geri getiriyoruz. Yine de tek gerçek dayanağımız, kritik verinin asıl evinin PostgreSQL olması.
Ölçek büyürken öğrendiğimiz tuzaklar
Redis basit görünür, ama ölçek büyüdükçe birkaç tuzak kendini gösterdi. Birincisi bellek yönetimi. Redis belleğe sığdığı sürece hızlıdır; bellek dolduğunda ise yapılandırdığınız tahliye politikasına göre anahtar atmaya başlar. Önbellek amaçlı kullanımda "en az kullanılanı at" politikası mantıklıyken, kilit ve rezervasyon gibi kaybı tolere edilemez veriler için yanlış politika sessiz hatalara yol açar. Bu yüzden farklı kullanım desenlerini farklı mantıksal veritabanlarında ya da ayrı örneklerde tutmak, tek bir politikanın herkese dayatılmasını önledi.
İkinci tuzak "sıcak anahtar" ve "büyük anahtar" problemi. Tek bir anahtar çok sık okunuyorsa ya da tek bir yapı aşırı büyükse, o anahtar bir darboğaza dönüşür. Sepet gibi kullanıcı başına dağılan yapılarda bu sorun kendiliğinden çözülür; ama küresel bir sayaç ya da dev bir liste tuttuğunuzda dikkatli olmak gerekir. Büyük yapıları parçalamak ve sıcak okumaları uygulama içinde kısa süreli tamponlamak, bu darboğazları dağıttı.
Üçüncüsü izleme. Redis'in ne kadar bellek kullandığını, hangi anahtarların süresinin dolduğunu ve komut gecikmelerini sürekli ölçmeden büyütmek risklidir. Bu metrikleri panoya taşıyıp eşik alarmları kurduğumuzda, bir sorun kullanıcıya yansımadan önce fark edilir hâle geldi. Redis'i "kur ve unut" bir bileşen gibi görmek, en sık yapılan ve en pahalıya patlayan hatalardan biri.
Sonuç
Redis'i her iki üründe de değerli kılan, hızlı olması değil, doğru problemi çözmesiydi: çok sık değişen, kısa ömürlü ve anlık okunması gereken durumu yönetmek. Randevu kilitleri, terapist müsaitliği, sepet ve stok rezervasyonu — hepsi aynı desende çözüldü: atomik işlemler, otomatik süre dolması ve gerçek zamanlı yayın. Anahtar disiplin, Redis'i bir veritabanı gibi kullanma cazibesine direnmekti. Gerçeğin kaynağını PostgreSQL'de tutup, "şu an ne oluyor?" sorusunu Redis'e bırakınca, iki dünyanın da en iyi yanını aldık: kalıcılık ve hız. Bir sonraki durum probleminizde, önce hangi verinin gerçekten kalıcı olması gerektiğini sorun; geri kalanı çoğu zaman Redis'in evindedir.