Kısım II · İnşaat · Bölüm 40
Önbellek ve Redis
Caching and Redis
Neden burada: Bölüm 39 sonunda bir soru bıraktı: dizin sayesinde sorgu 0,05 milisaniyeye indi, ama yüzlerce kullanıcı leaderboard'u saniyede defalarca açarsa, her açılışta Postgres'e gidip aynı sıralamayı yeniden hesaplatmak gereksiz iştir. Bölüm 8'de bilgisayarın en temel pazarlığını görmüştün: hızlı şey küçük ve pahalı, yavaş şey büyük ve ucuzdur; bu yüzden sık kullanılanı yakına koyarız. Bu bölüm o fikri arka uç ölçeğine taşır: veritabanının önüne, RAM'de çalışan hızlı bir ara depo.
Neye dayanıyor: Bölüm 8.2 (önbellek fikri, isabet ve ıska, yerellik) ve 8.4 (aynı fikir üst katlarda tekrarlanır; orada Redis'e söz verilmişti), Bölüm 39 (kalıcı ve doğru veri Postgres'te durur; işlem ve kısıtlar), ve Bölüm 11 (sözlük, ağaç, sıralı yapılar: sıralı küme bunların birleşimidir).
- Kat 10Bizim uygulamamızGo sunucusu, Redis'e leaderboard, oturum ve sayaç için başvurur. Bu bölümün işi.
- Kat 8AğRedis de ayrı bir süreçtir; Go ona TCP üzerinden bağlanır, ama aynı makinede ya da aynı ağda olduğu için gidiş-geliş çok kısadır (bkz. Bölüm 19).
- Kat 7İşletim sistemiRedis bir süreç olarak çalışır; belleği ve (isteğe bağlı) disk dosyalarını işletim sisteminden alır (bkz. Bölüm 13, Bölüm 15).
- Kat 3Bellek (RAM)Önbelleğin hızı, verinin RAM'de durmasından gelir; bedeli kapasitenin küçüklüğü ve uçuculuktur (bkz. Bölüm 8).
Bu bölümün sonunda Redis'in ne olduğunu ve neden Postgres'in yerine değil yanına konduğunu açıklayabilirsin; sıralı kümenin neden leaderboard için doğal bir yapı olduğunu söylersin; "cache-aside" kalıbını, yaşam süresini (TTL) ve geçersiz kılma sorununu bir hukuk uygulamasının örneğiyle anlatırsın; oturum, hız sınırlama ve kuyruk gibi başka işlerin de aynı araçla nasıl yapıldığını görürsün; ve belki en önemlisi, ne zaman önbellek kurmaman gerektiğini bilirsin.
Önceki bölüm gibi, buradaki komut çıktıları gerçektir: Redis 8 serisinden bir sürümü (denediğim kurulum 8.10.3) konteynerde çalıştırdım, komutları redis-cli ile verdim ve Go kodlarını go-redis ile derleyip çalıştırdım.
40.1 Bellek içi veri deposu (in-memory data store); anahtar–değer (key–value)
Bölüm 8'deki gecikme tablosunu hatırla: RAM'e erişmek ile diske ya da ağdaki bir makineye gitmek arasında kat kat, büyüklük basamakları kadar fark vardı. Postgres güvenilirliği için verisini diske yazar (kalıcılık, Bölüm 39.3); disk güvenliğin bedelidir. Peki "kalıcı olması şart olmayan, ama çok hızlı olması şart" veriler için? Örneğin "şu an haftanın ilk 10 kullanıcısı kim" ya da "bu kullanıcının bu dakikadaki istek sayısı". Bunlar yeniden hesaplanabilir ya da kaybolsa büyük zarar vermeyen verilerdir.
Cevap: verinin bütün çalışma kümesini RAM'de tutan bir depo. Buna bellek içi veri deposu (in-memory data store) denir. Redis bunların en yaygınıdır. Resmî belgesi Redis'i "bir veri yapısı sunucusu" (data structure server) olarak tanıtır: çekirdeğinde, önbellekten kuyruğa ve olay işlemeye kadar çeşitli sorunları çözmek için yerleşik veri tipleri vardır.1
Anahtar–değer modeli
Postgres'te veri tablolarda, satırlarda ve sütunlardadır; sorgular SQL'dir. Redis'te model çok daha yalındır: anahtar–değer (key–value). Her şey bir anahtar (bir ad) ile ona bağlı bir değer çiftidir. Bölüm 11'deki sözlük (hash table) fikrinin ağ üzerinden erişilen halidir: "oturum:abc → şu kullanıcı". Bir şeyi bulmak için anahtarın adını bilmen gerekir; "bütün 5 puandan fazla olanlar" gibi bir sorgu yoktur (bunun için sorgu dili olan veritabanı gerekir). Bu kısıtlama, hızın bedelidir ve aynı zamanda sadeliğin kaynağıdır.
Redis'in değerleri yalnızca "metin" değildir; değerin kendisi bir veri yapısı olabilir. Bu, 40.2'nin konusu. Önce Redis'le nasıl konuşulduğunu görelim. Terminalden redis-cli aracıyla:
SET sess:abc user:1 EX 2 -- "sess:abc" anahtarına "user:1" değerini yaz, 2 saniye sonra sil
OK
GET sess:abc
user:1
-- ... 2 saniye sonra:
GET sess:abc
(nil) -- yok: süresi doldu, Redis sildi
Bir de bir hata gösterelim, çünkü Redis'in "türlü" bir model olduğunu söyler: SET n abc ile n'ye bir metin yazıp sonra aynı anahtara liste komutu (LPUSH n x) verirsem cevap şu olur: WRONGTYPE Operation against a key holding the wrong kind of value. Her anahtarın bir tipi var ve komut tipe uymak zorunda.
Redis kalıcı mı? Postgres'in yerine geçer mi?
Redis verisini RAM'de tutar, ama istenirse diske de yazabilir. Belgesi seçenekleri sayar: RDB (belirli aralıklarla verinin anlık görüntüsünü alma), AOF (her yazma komutunu bir dosyaya ekleme, açılışta tekrar oynatma), ikisinin birleşimi, ya da hiç kalıcılık olmaması; belge, kalıcılığı tamamen kapatmanın önbellek olarak kullanımda bazen tercih edildiğini de belirtir.2 RDB'nin zayıf yanı da açıkça yazılı: elektrik kesintisi gibi bir durumda veri kaybı olasılığını en aza indirmek istiyorsan RDB iyi değildir; son görüntüden sonraki yazmalar gidebilir.2 Denediğim varsayılan kurulumda AOF kapalıydı (appendonly no) ve yalnızca RDB kaydetme kuralları vardı (save 3600 1 300 100 60 10000: örneğin "60 saniye içinde 10.000 değişiklik olduysa kaydet").
Hukuk uygulamasında bir kullanıcının toplam puanı, tamamladığı bölümler, hesabı Postgres'te durur. Redis'te tutulan şey ya türetilmiştir (Postgres'ten yeniden kurulabilir: leaderboard) ya da geçicidir (oturum, hız sayacı). Redis'in boşalması bir felaket olmamalı, bir yeniden ısınma olmalı. Bu kural, aşağıdaki bütün tasarım kararlarının dayanağıdır.
40.2 Redis veri yapıları: string, hash, list, set, sıralı küme (sorted set)
Redis'in sunduğu veri tipleri belgede tek tek sayılır: strings, hashes, lists, sets, sorted sets, ayrıca bitmaps, geospatial, streams, JSON, olasılıksal tipler vb.3 Bizim işimize beş tanesi yarıyor. Hepsini Bölüm 11'de gördüğün yapılarla eşleyelim:
| Tip | Ne | Bölüm 11'deki karşılığı | Bizde ne için |
|---|---|---|---|
| string (string) | Bayt dizisi; metin, sayı, serileştirilmiş JSON olabilir. Aynı zamanda sayaç olarak artırılabilir.4 | değişken | önbelleğe alınmış JSON yanıt, oturum, sayaç |
| hash (hash) | Alan–değer çiftleri koleksiyonu; belgeye göre basit nesneleri ve sayaç gruplarını temsil etmek için.5 | kayıt / sözlük | kullanıcının önbellek kaydı: username, streak |
| list (list) | Ekleme sırasına göre dizilmiş metin değerleri; belge yığın ve kuyruk gerçeklemek için yaygın olduğunu söyler.6 | bağlı liste, kuyruk | arka plan iş kuyruğu |
| küme (set) | Sırasız, tekrarsız metin değerleri.7 | küme | "şu an çevrimiçi kullanıcılar" |
| sıralı küme (sorted set) | Her üyeye bir skor bağlı, skora göre sıralı, tekrarsız üyeler.8 | sıralı yapı | leaderboard |
Sıralı küme: leaderboard için biçilmiş kaftan
Belgenin tanımı: sıralı küme, her biri bir skorla ilişkilendirilmiş benzersiz metinlerin (üyelerin) koleksiyonudur; aynı skora sahip üyeler sözlük sırasına (lexicographic) göre sıralanır. Belge kullanım alanı olarak ilk sırada leaderboard'ları, ikinci sırada hız sınırlayıcıları sayar.8 Bunu bir küme ile bir sözlüğün karışımı olarak düşünebilirsin: küme gibi her üye bir kez geçer, sözlük gibi her üyenin bir değeri (skoru) vardır; üstelik hepsi skora göre sürekli sıralı durur.
Nasıl sıralı kalıyor? Belge uygulama notunu açıkça yazar: sıralı kümeler hem bir skip list hem bir hash table içeren ikili bir veri yapısıyla gerçeklenir; her eleman eklendiğinde Redis O(log N) bir işlem yapar, sıralı eleman istenince hiçbir iş yapmaz, çünkü eleman zaten sıralıdır.8 (O(log N): Bölüm 11'de ikili aramadan bildiğin, "N iki katına çıkınca bir adım daha" büyüme; 1 milyon üyede yaklaşık 20 adım.)
Gerçek bir oturum. Dört kullanıcı:
ZADD lb:weekly 120 ayse 95 mehmet 210 kerem 95 zeynep -- skor üye çiftleri; 4 yeni üye
(integer) 4
ZRANGE lb:weekly 0 2 REV WITHSCORES -- en yüksek 3, yüksekten düşüğe
kerem 210
ayse 120
zeynep 95
ZINCRBY lb:weekly 50 mehmet -- mehmet'e 50 puan ekle, yeni skoru döndür
145
ZREVRANK lb:weekly mehmet -- sıra (0 tabanlı, yüksekten düşüğe)
1
ZREVRANK lb:weekly kerem
0
ZSCORE lb:weekly mehmet
145
ZCARD lb:weekly -- üye sayısı
4
İki şeye dikkat. Birincisi: mehmet ve zeynep ikisi de 95 puandı; ZRANGE ... REV çıktısında zeynep öne geçti. Beraberlikte kural sözlük sırasıdır (belgeye göre sözlük sırasında büyük olan büyük sayılır8) ve ters sırada okurken "z" "m"den önce gelir. Bu bir sürpriz olabilir; gerçek hayatta beraberliği kimin bozacağına ürün olarak karar vermelisin (örneğin "önce ulaşan kazanır" için skora zaman damgasından bir kesir eklemek gibi). İkincisi: ZINCRBY tek bir atomik komut: tıpkı Postgres'te SET total_xp = total_xp + n gibi, iki eşzamanlı artış birbirini ezmez (Bölüm 39.3). Belge de ZADD'in skoru güncellediğini, ZINCRBY'nin puan eklemek için olduğunu ve leaderboard'ların bu yüzden doğal kullanım alanı olduğunu söyler.8
Aynı işlem Go'dan şöyle. Bu kodu derleyip çalıştırdım (github.com/redis/go-redis/v9 istemcisiyle):
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"}) // Redis'e bağlantı
rdb.ZAdd(ctx, "lb", redis.Z{Score: 120, Member: "ayse"}, redis.Z{Score: 210, Member: "kerem"})
rdb.ZIncrBy(ctx, "lb", 50, "ayse") // ayse: 120 -> 170
top, _ := rdb.ZRevRangeWithScores(ctx, "lb", 0, 9).Result() // ilk 10, yüksekten düşüğe
for i, z := range top {
fmt.Println(i+1, z.Member, z.Score) // 1 kerem 210 / 2 ayse 170
}
rank, _ := rdb.ZRevRank(ctx, "lb", "ayse").Result() // 1 (0 tabanlı: ikinci sıra)
Peki leaderboard'u neden Postgres'te yapmıyoruz?
Yapabiliriz ve küçük ölçekte yapılır: SELECT username, total_xp FROM users ORDER BY total_xp DESC LIMIT 10 ve total_xp üzerinde bir dizin (Bölüm 39.2) ilk 10'u hızlıca getirir. Zorluk iki yerde. Birincisi, "ben kaçıncıyım?" sorusu: "benden yüksek puanlı kaç kişi var?" demektir ve bunu saymak gerekir; sayma, dizin üzerinde bile benden yukarıdaki girdilerin üzerinden geçmektir. Sıralı kümede ZREVRANK aynı cevabı O(log N)'de verir.8 İkincisi, yazma yükü: her doğru cevapta puan değişir; her değişiklik hem tabloya hem dizine yazılır, hem de bu sırada herkes leaderboard'u okur. Önceki bölümde MVCC sayesinde okumanın yazmayı bloke etmediğini gördük, doğru; ama her okuma yine de iş yapar. Redis'te hepsi RAM'de ve tek komuttur.
Bir kullanıcı doğru cevap verince akış (kabaca, ayrıntı Bölüm 45'te): Go sunucusu Postgres işlemini onaylar (puan ve cevap kaydı kalıcı), ardından Redis'te ZINCRBY lb:haftalik:2026-W46 10 kullanıcı-1 çalıştırır. Leaderboard ekranı yalnızca Redis'ten okur: ZRANGE ... REV WITHSCORES (ilk 10) ve ZREVRANK (benim sıram). Haftalık sıralama için anahtara hafta numarasını gömmek, haftanın sonunda anahtara EXPIRE vermek yeter; "bu hafta" sıfırlanır, Postgres'teki toplam puan etkilenmez.
Postgres önce, Redis sonra yazılır çünkü doğru olan Postgres'tir. Ama bu iki yazma aynı işlemde değildir: Postgres onaylandıktan sonra Redis çağrısı başarısız olursa leaderboard geride kalır. Bunu kabul edilebilir kılan tasarım: Redis'teki leaderboard Postgres'ten yeniden kurulabilir olmalı (örneğin gece çalışan bir betik users'tan okuyup ZADD ile yeniden doldurur). Bu "iki yere yazma" meselesi mimari bir tuzak olarak Bölüm 44'te yeniden karşına çıkacak.
40.3 Önbellek stratejileri: cache-aside, yaşam süresi (TTL), geçersiz kılma (invalidation)
Önbellek denince akla sıralı küme değil, daha düz bir kullanım gelir: pahalı bir sonucu bir kez hesapla, sonra tekrar tekrar sakladığın yerden ver. Bölüm 8.2'deki terimler burada aynen geçerli: istenen şey önbellekte varsa isabet (hit), yoksa ıska (miss). Sorun şu: önbellekteki kopya, asıl veri değiştiğinde eskir (stale). Her strateji, bu eskime ile hız arasındaki bir pazarlıktır.
Cache-aside (lazy loading)
Cache-aside (lazy loading, tembel yükleme) en yaygın kalıp. AWS'nin ElastiCache belgesi onu şöyle tarif eder: veriyi yalnızca gerektiğinde önbelleğe yükleyen strateji; uygulama önce önbelleğe sorar; veri orada ve geçerliyse onu döndürür; yoksa ya da süresi dolmuşsa veriyi asıl veri deposundan (veritabanı) ister.9 Adımlar:
- İstek gelir. Önce Redis'e bak (
GET). - Isabet: bulundu, döndür.
- Iska: Postgres'ten oku, sonucu Redis'e yaz (bir yaşam süresiyle), döndür.
Gerçek bir Go parçası (Postgres'ten "ilk 3 kullanıcı"yı çekip Redis'te 30 saniye saklıyor; ikisini de çalıştırdım):
func TopCached(ctx context.Context, rdb *redis.Client, db *pgxpool.Pool) ([]Entry, string, error) {
const key = "cache:top3"
raw, err := rdb.Get(ctx, key).Result() // 1) önce önbelleğe sor
if err == nil { // isabet
var out []Entry
return out, "isabet", json.Unmarshal([]byte(raw), &out)
}
if !errors.Is(err, redis.Nil) { // redis.Nil = "yok"; başka bir hata gerçek hata
return nil, "", err
}
rows, err := db.Query(ctx, `SELECT username, total_xp FROM users ORDER BY total_xp DESC, username LIMIT 3`)
// ... satırları oku, out dilimine doldur ...
b, _ := json.Marshal(out)
rdb.Set(ctx, key, b, 30*time.Second) // 3) Redis'e yaz, 30 sn yaşasın
return out, "ıska", rows.Err()
}
Art arda iki çağrıdaki çıktı: ilkinde ıska [{u1 25} {u10 0} {u100 0}], ikincisinde isabet ve aynı veri; ardından TTL: 30s. İlk istek yavaş yolu (Postgres) gitti ve önbelleği doldurdu; ikinci istek önbellekten karşılandı.
Bu kalıbın iyi yanı: yalnızca istenen veri önbelleğe girer ve (Redis hatalarını yutacak şekilde yazılırsa; yukarıdaki örnek hatayı olduğu gibi döndürüyor, üretimde Redis hatasında Postgres'e düşmek tercih edilir) Redis çökse bile uygulama yavaşlar ama çalışmaya devam eder: her şey ıskaya düşer. Kötü yanı belge de söyler: veritabanında veri değiştiğinde önbellekte güncelleme olmaz; bu yüzden veri bayatlayabilir.9
Yaşam süresi (TTL)
Bayatlamaya karşı en basit önlem: her önbellek girişine bir ömür vermek. Yaşam süresi (TTL, time to live), bir anahtarın silinmeden önce kalacağı süredir. Redis'te SET anahtar değer EX saniye ya da ayrı bir EXPIRE komutuyla verilir; TTL komutu kalan süreyi gösterir (süresi dolmuş/yok anahtar için -2 döndüğünü gördüm). Redis süresi dolan anahtarları iki yolla siler: pasif (birisi o anahtara eriştiğinde süresi dolmuşsa silinir) ve aktif (Redis periyodik olarak süresi olan anahtarlardan rastgele bir kısmını yoklar ve dolmuşları siler).10 Yani süre dolduğu anda anahtar o milisaniyede yok olmayabilir ama okuyana asla bayat sürüm sunulmaz.
TTL bir pazarlıktır: kısa TTL = taze veri, çok ıska; uzun TTL = çok isabet, bayat veri. Leaderboard için 30 saniyelik bir bayatlık çoğu kullanıcının fark etmeyeceği bir şeydir; bir kullanıcının bakiyesi için kabul edilemez bir şey olabilir. Hangi verinin ne kadar bayat olabileceği bir ürün kararıdır, teknik değil.
Geçersiz kılma (invalidation)
TTL'e güvenmek yerine, veri değiştiğinde önbellek girişini açıkça silmek ya da güncellemek de mümkündür: geçersiz kılma (invalidation). Örneğin bir sorunun metni düzeltilince DEL cache:soru:77; sonraki istek ıskaya düşer ve yeni metin yüklenir. AWS belgesinin sunduğu ikinci yol write-through: veriyi veritabanına her yazışta önbelleği de güncellemek; avantajı önbelleğin bayatlamaması, dezavantajı her yazmanın iki yere gitmesi (ek gecikme) ve yeni bir önbellek düğümü açılınca verinin eksik olmasıdır.9 Gerçek hayatta iki yaklaşım karıştırılır: yazarken sil + TTL'i emniyet kemeri olarak bırak.
Zor yer şurası: önbellekte tuttuğun şey asıl verinin türevi olduğunda (örneğin "ilk 10 listesi"), hangi değişikliğin hangi girişleri etkilediğini bilmek gerekir. Doğru geçersiz kılmayı yazmak, önbelleğin en çok hata çıkaran yanıdır. Basit çözüm genelde en iyisidir: kısa TTL ile yetin; gerçekten gerekmedikçe karmaşık silme mantığı kurma.
Bellek dolunca: tahliye (eviction)
RAM sınırlıdır. Redis'e maxmemory ile bir üst sınır verirsin; sınır dolunca ne olacağını tahliye politikası belirler. Belgeye göre örneğin noeviction yeni veri yazmayı hata ile reddeder (okumalar çalışmaya devam eder); allkeys-lru en az yakın zamanda kullanılan anahtarları siler ve belge, bazı elemanlar diğerlerinden çok daha sık erişiliyorsa (çok yaygın durum) iyi bir varsayılan seçenek diye öneri verir; volatile-* politikaları yalnızca TTL'i olan anahtarları siler.11 Denediğim kurulumun varsayılanı noeviction'dı: yani önbellek olarak kullanacaksan bu ayarı bilerek seçmelisin. Bu, Bölüm 8'deki önbellek yer değiştirme fikrinin aynısıdır: sınırlı yere neyi koyacağına karar vermek.
Aday listesi: bölüm içeriği ve soru metinleri (sık okunur, nadiren değişir: uzun TTL + içerik güncellenince DEL); leaderboard (sıralı küme, 40.2); kullanıcı profil özeti. Önbelleğe alınmayacaklar: puanın kendisi, cevabın doğruluğu, oturum sonrası ödül hesabı; yani doğru sonucu tek bir yerden (Postgres) veren her şey. Ön yüzde TanStack Query (Bölüm 33) de kendi önbelleğini tutar: aynı fikir, bir kat yukarıda, kullanıcının telefonunda. Üç kat ardışık önbellek varsa (telefon, Redis, Postgres'in kendi bellek önbelleği) bayatlık her katta ayrı hesaplanır.
40.4 Oturum saklama, hız sınırlama (rate limiting), kuyruk (queue)
Redis'in hızı ve TTL'i, önbelleğin dışında üç işe daha çok uygun düşer. Üçü de aynı iki özelliğe dayanır: geçici veri ve atomik tek komutlar.
Oturum saklama
Kullanıcı giriş yaptığında sunucu ona bir oturum kimliği (session ID) verir; sonraki isteklerde bu kimlik gelir ve sunucu "bu kimlik kimin?" diye bakar. Bölüm 34.4'te durumsuz ve durumlu sunucuyu konuşmuştuk: oturum bilgisini sunucunun kendi belleğinde tutarsan, sunucuyu iki kopyaya çıkardığında kullanıcı bir kopyadan öbürüne geçince oturumunu kaybeder. Çözüm, oturumu ortak bir yerde tutmak: Redis. Anahtar sess:<kimlik>, değer kullanıcı numarası, TTL oturumun ömrü (yukarıdaki SET ... EX denemem tam bu). Kullanıcı çıkış yapınca DEL; ya da süre dolunca kendiliğinden silinir. Oturumun güvenlik yanını Bölüm 41'de göreceğiz.
Hız sınırlama (rate limiting)
Bir kullanıcının (ya da bir IP'nin) dakikada kaç istek yapabileceğini sınırlamak, hem kötüye kullanımı hem şifre deneme saldırılarını (Bölüm 41) yavaşlatır. Redis belgesinin INCR sayfası bu kalıbı "sayaç kalıbının özel hali" olarak anlatır ve iki sürüm verir. İlki: anahtar IP + saniye; her istekte INCR ile sayacı artır, ilk artışta EXPIRE ver, sayaç sınırı geçtiyse isteği reddet; INCR ve EXPIRE'ın birlikte çalışması için MULTI/EXEC sarmalayıcısı kullanılır.12 Aynı belge ikinci sürümün (önce GET, sonra INCR ve EXPIRE) bir yarış durumu içerdiğini açıkça yazar: INCR çalışıp EXPIRE çalışmazsa anahtar sızar (süresiz kalır); çözüm olarak ikisini bir Lua betiğine koymayı önerir.12 Yani Bölüm 14.4'ün yarış durumu burada da aynen var; "atomik tek komut" ya da komut grubu çözümdür.
INCR rl:ip1 -- (integer) 1 ilk istek: sayaç 1 oldu
EXPIRE rl:ip1 60 -- (integer) 1 60 saniye ömür ver
INCR rl:ip1 -- (integer) 2 ikinci istek
TTL rl:ip1 -- (integer) 60 (komutlar arasında biraz geçtiyse 59 gösterir)
Mantık: sayaç sınırı (örneğin dakikada 60) aşarsa 429 Too Many Requests gibi bir durum kodu (Bölüm 35.2) dön. Süre dolunca anahtar kendiliğinden gider ve pencere sıfırlanır.
Kuyruk (queue)
Bazı işler isteğin içinde yapılmamalıdır: e-posta göndermek, haftalık özet hesaplamak. Sunucu işi bir kuyruğa koyar ve hemen yanıt verir; ayrı bir süreç ("işçi", worker) kuyruktan alıp yapar. Redis listeleri bunun için kullanılır: belge, listelerin yığın ve kuyruk gerçeklemek ve arka plan işçi sistemleri için sıra yönetimi kurmak için yaygın olduğunu söyler ve LPUSH ile eklenip RPOP ile alınan ilk giren ilk çıkar (FIFO) kalıbını gösterir.6 Denemem:
LPUSH jobs j1 -- (integer) 1
LPUSH jobs j2 -- (integer) 2
RPOP jobs -- "j1" ilk giren (j1) ilk çıktı
Liste belgesinde ayrıca BRPOP gibi bloklayan sürümler vardır: kuyruk boşsa işçi sürekli sormak (polling) yerine bir zaman aşımına kadar bekler.6 Kuyruğun ayrıntıları ve olay güdümlü mimariyle ilişkisi Bölüm 44.5'te. Bir uyarı: Redis listesi basit işler içindir; işin kaybolmaması garanti edilmesi gereken sistemler için (ödeme gibi) özel kuyruk sistemleri ve kalıcılık ayarları gerekir.
40.5 Ne zaman önbellek gerekmez
Önbellek bir karmaşıklık ekler: iki yerde veri, bayatlık, geçersiz kılma, bir işletilecek servis daha. Bu bedeli ancak karşılığı olan yerde ödemelisin. Önbelleği kurmadan önce şu soruları sor:
- Gerçekten yavaş mı? Ölç. Bölüm 39'da dizinle 14 ms'lik sorgu 0,05–0,15 ms'ye indi. Bu hızda bir sorgu için Redis'i araya koymak, yeni bir ağ gidiş-gelişi ekleyerek kazandığından fazlasını kaybettirebilir. Önce
EXPLAINve dizin; sonra önbellek. - Veri sık okunuyor mu, nadiren mi değişiyor? Önbellek, çok okunan ve az değişen veri için işe yarar. Her istekte farklı ve kullanıcıya özel veriyi önbelleğe almak isabet oranını sıfıra yaklaştırır.
- Bayatlık kabul edilebilir mi? Puan bakiyesi, ödeme durumu, "kullanıcı bu cevabı verdi mi" gibi doğruluğu kritik veriler önbelleğe alınmaz ya da çok dikkatle alınır.
- Bir hatanın bedeli ne? Önbellek bozuk bir değer tutuyorsa herkes onu görür; kaynağı Postgres'te olsa bile.
- Ölçeğe ulaştık mı? Birkaç yüz kullanıcılı bir ilk sürümde Postgres tek başına yeter. Önbelleği ihtiyaç ortaya çıkınca ekle; sorun kanıtlanmadan eklenen önbellek, "erken optimizasyon"dur.
Önbellekle ilgili hatalar genellikle "bazen, bazı kullanıcılarda, bir süre sonra düzeliyor" gibi tarif edilir; çünkü TTL dolunca kendiliğinden iyileşirler. Agent'ın yazdığı kodda önbellek gördüğünde sorman gerekenler: anahtarın adı neleri ayırıyor (kullanıcıya özel veri ortak bir anahtara yazılıyor mu?), TTL var mı, veri değişince ne oluyor? Kullanıcıya özel yanıtı kullanıcıdan bağımsız bir anahtarla önbelleğe almak, bir kullanıcının verisini başkasına göstermeye yol açabilir: bu bir güvenlik hatasıdır (Bölüm 41).
Bölüm 8'de önbellek fikrini RAM ile disk, L1 ile RAM arasındaki fark olarak gördün: "sık kullanılanı yakına koy". Redis aynı fikrin uygulama düzeyindeki hali: yakın = Redis'in RAM'i, uzak = Postgres'in diski ve sorgu planı. Terimler aynı kaldı: isabet, ıska, yerellik (az sayıda anahtar çok sık sorulur), tahliye. Fark, burada veri tutarlılığını sen yönetiyorsun: donanım önbelleği bayatlamayı kendisi çözer, uygulama önbelleği çözmez.
Bu bölümde neler kuruldu
Redis, veriyi RAM'de tutan, anahtar–değer modelli bir veri yapısı sunucusudur; hızı RAM'den, bedeli kapasite ve (varsayılan ayarlarda) zayıf kalıcılıktan gelir; bu yüzden doğrunun kaynağı Postgres kalır. Veri tipleri string, hash, list, set ve sıralı kümedir; sıralı küme skor ve sıra işlemlerini O(log N)'de yaparak leaderboard için doğal yapıdır (ZADD, ZINCRBY, ZRANGE ... REV, ZREVRANK). Cache-aside kalıbı önce önbelleğe, ıskada veritabanına bakar; TTL bayatlığı sınırlar; geçersiz kılma değişiklikte girişi siler ya da günceller; bellek dolunca tahliye politikası devreye girer. Aynı araçlar oturum saklama (SET ... EX), hız sınırlama (INCR + EXPIRE) ve kuyruk (LPUSH/RPOP) için de kullanılır. Önbellek ancak ölçülmüş bir ihtiyaç varsa, bayatlığı kabul edilebilir veri için kurulur.
Terimler sözlükte, Bölüm 40 başlığı altında.
Redis belgeleri, Sorted sets (giriş ve "Updating the score: leaderboards" kısmı). Ardından Key eviction (tahliye politikaları) ve INCR sayfasındaki "Pattern: Rate limiter" bölümü. Derinleşmek için AWS, Caching strategies (lazy loading ve write-through).
Takıldığın her yeri yapay zekâ öğretmenine sor. Bir alıştırma önerisi: ondan yerel bir Redis'te beş kullanıcılı bir leaderboard kurmasını, sonra iki kullanıcıyı aynı skora getirip sıralamayı ve beraberlik kuralını sana göstermesini iste; ardından "beraberlikte önce ulaşan kazansın" kuralını skor tasarımıyla nasıl çözeceğini tartışın.
Bölüm sonu soruları
Kitaba bakmadan cevapla. Kâğıt kalem serbest.
ZREVRANK sırayı O(log N)'de verir. Bkz. 40.2.INCR ile EXPIRE arasında bir yarış durumu neden sorundur?INCR çalışıp EXPIRE çalışmazsa anahtar süresiz kalır; belge çözüm olarak MULTI/EXEC ya da Lua betiğini önerir. Bkz. 40.4.EXPLAIN ile çöz; ihtiyaç kanıtlanınca ekle. Bkz. 40.5.Örnek cevabı göster
Postgres onaylandığı için kullanıcının gerçek puanı doğrudur, ama Redis'teki sıralama geride kalır. İki yazma aynı işlemde değildir. Redis'teki leaderboard'u türetilmiş ve yeniden kurulabilir saymak (periyodik olarak Postgres'ten ZADD ile yeniden doldurmak) ve kısa süreli tutarsızlığı ürün olarak kabul etmek çözümdür.
cache:profile adlı tek bir anahtarla 10 dakika önbelleğe alan bir kod yazdı. Ne ters gidebilir, nasıl düzeltirsin?Örnek cevabı göster
Anahtar kullanıcıdan bağımsız olduğu için ilk isteyenin profili sonraki herkese gösterilir: bayat veri ve bir kullanıcının verisinin başkasına sızması. Anahtara kullanıcı numarası eklenir (cache:profile:<id>), TTL kısaltılır ve profil değişince anahtar silinir; ya da bu veri hiç önbelleğe alınmaz.
Kaynaklar
- Redis belgeleri, Redis data types: "Redis is a data structure server", yerleşik veri tipleri listesi.
- Redis belgeleri, Redis persistence: RDB, AOF, kalıcılığı kapatma; RDB'nin veri kaybı konusundaki dezavantajı. Varsayılan ayarlar bölümde kullanılan Redis konteynerinde
CONFIG GETile okundu. - Redis belgeleri, Redis data types (aynı sayfa): tip listesi ve açıklamalar.
- Redis belgeleri, Redis strings: bayt dizisi, önbellek ve sayaç kullanımı.
- Redis belgeleri, Redis hashes: alan–değer çiftleri.
- Redis belgeleri, Redis lists: bağlı liste, kuyruk kalıbı (
LPUSH/RPOP),BRPOP. - Redis belgeleri, Redis sets: sırasız, benzersiz üyeler.
- Redis belgeleri, Redis sorted sets: skorlu üyeler, sözlük sırası, skip list + hash table, leaderboard ve ZINCRBY.
- Amazon Web Services, Caching strategies (ElastiCache): lazy loading, write-through, TTL.
- Redis belgeleri, EXPIRE: anahtarların pasif ve aktif sona erdirilmesi.
- Redis belgeleri, Key eviction:
maxmemory, tahliye politikaları (noeviction,allkeys-lru,volatile-*). - Redis belgeleri, INCR: sayaç kalıbı, "Pattern: rate limiter".