Go'da Redis için resmî istemci go-redis'tir: bir Go modülü içinde go get github.com/redis/go-redis/v9 ile kurar, redis.NewClient(&redis.Options{Addr: "localhost:6379"}) ile istemci oluşturur ve her komuta ilk argüman olarak bir context.Context verirsiniz. Komut metotları doğrudan değer değil bir komut nesnesi döndürür; sonucu ve hatayı Result() ile, yalnızca hatayı Err() ile alırsınız. Go'ya özgü en önemli ayrıntı, anahtar bulunamadığında dönen redis.Nil değeridir: bu bir hata değeri gibi döner ama çoğu zaman "veri yok" anlamına gelir ve ayrı ele alınmalıdır.

Bu yazıda go-redis ile bağlantıyı, URL'den yapılandırmayı, redis.Nil ve hata türlerini, pipeline ile TxPipelined transaction'larını, Watch ile iyimser kilidi, bağlantı havuzu ve ayrı pipeline havuzu ayarlarını, otomatik pipelining'i ve üretim önerilerini resmî belgelere dayanarak anlatıyoruz. Yazı Go dilini değil, Redis kütüphanesini konu alıyor; dil tarafı için Go öğrenme rehberi yazısına bakabilirsiniz. Dile bağlı olmayan URL ve hata yönetimi kuralları Redis bağlantı URL ve connection pool yazısında, serinin geneli Redis öğrenme rehberi sayfasında.

go-redis kurulumu

Belgelere göre go-redis Go'nun son iki sürümünü destekler ve yalnızca bir Go modülü içinden kullanılabilir. Bu yüzden önce bir modül başlatır ya da kodunuzu mevcut bir modüle eklersiniz, ardından v9 sürümünü indirirsiniz:

bash
go mod init github.com/my/repo
go get github.com/redis/go-redis/v9

go-redis ile Redis'e bağlanma

Bağlantı ayarları redis.Options yapısıyla verilir. Belgelerdeki temel örnekte adres, şifre, veritabanı numarası ve protokol sürümü açıkça yazılmıştır:

go
import (
	"context"
	"fmt"
	"github.com/redis/go-redis/v9"
)

func main() {
    client := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // No password set
        DB:       0,  // Use default DB
        Protocol: 2,  // Connection protocol
    })
    defer client.Close()

    ctx := context.Background()

    err := client.Set(ctx, "foo", "bar", 0).Err()
    if err != nil {
        panic(err)
    }

    val, err := client.Get(ctx, "foo").Result()
    if err != nil {
        panic(err)
    }
    fmt.Println("foo", val)
}

Kodda üç ayrıntıya dikkat edin. Set() çağrısının son argümanı son kullanma süresidir ve 0 süresiz demektir. Bağlantıyı açtığınız fonksiyonun sonunda kapatmak için belgeler bağlantıdan hemen sonra defer client.Close() yazmayı önerir. Son olarak her komut bir ctx alır; zaman aşımı ve iptal bu bağlam üzerinden çalışır.

ParseURL() ile URL'den bağlanma

Ayarları tek bir URL ile vermek için redis.ParseURL() kullanılır ve dönen seçenekler NewClient()'a geçirilir. Sorgu parametreleri redis.Options alanlarını yılan biçimli (snake case) adlarla ayarlar: dial_timeout, read_timeout, pool_size, client_name, protocol gibi. Süre alan parametreler saniye cinsinden bir sayı (5) ya da Go süre metni (500ms) kabul eder. Diğer istemcilerden farklı olarak go-redis tanımadığı her parametre için hata döndürür; bu, yazım hatalarını erken yakalamanızı sağlar ama aynı URL'yi başka bir dilin istemcisiyle paylaşırken sorun çıkarabilir.

go
opt, err := redis.ParseURL("rediss://default:<password>@<host>:6379/0?protocol=3")
if err != nil {
    panic(err)
}

client := redis.NewClient(opt)

rediss URL'sinde skip_verify=true sertifika doğrulamasını kapatır; bu parametre redis URL'sinde hatadır. Cluster için redis.ParseClusterURL() kullanılır ve diğer başlangıç düğümleri addr= parametreleriyle eklenir; bu fonksiyon yolu yok sayar ve db için hata döndürür. Sentinel için redis.ParseFailoverURL() vardır.

redis.Nil ve go-redis hata türleri

redis.Nil ve go-redis hata türleri: redis.Nil, context.DeadlineExceeded, net.OpError, redis.ResponseError

go-redis, Go'nun alışılmış hata döndürme yaklaşımını izler: komutun Result() metodu sonucu ve hata değerini birlikte döndürür. Belgeler, sonuçla ilgileniyor olsanız bile devam etmeden önce hatanın nil olduğunu her zaman kontrol etmenizi ister; başarısız bağlantılar, ağ sorunları ve geçersiz komut parametreleri hep bu değerle bildirilir.

HataNe zaman oluşurÖnerilen davranış
redis.NilAnahtar yokVarsayılan değer döndür veya kaynaktan getir
context.DeadlineExceededİşlem zaman aşımına uğradıBekleyerek yeniden dene
net.OpErrorAğ hatasıBekleyerek yeniden dene veya yedeğe dön
redis.ResponseErrorRedis hata yanıtı döndürdüKomutu veya argümanları düzelt

Belgelerdeki zarif gerileme örneği bu ayrımı gösterir: anahtar yoksa veritabanına gidilir, hata bir ağ hatasıysa uyarı yazılıp yine veritabanına dönülür, diğer her hata ise çağırana iletilir:

go
result, err := rdb.Get(ctx, key).Result()
if err != nil {
    if err == redis.Nil {
        // Key doesn't exist, try database
        return database.Get(ctx, key)
    }
    if _, ok := err.(net.Error); ok {
        // Network error, use fallback
        logger.Warn("Cache unavailable, using database")
        return database.Get(ctx, key)
    }
    return err
}
return result, nil

Belgelerdeki kullanım örneklerinde aynı kontrol errors.Is(err, redis.Nil) biçiminde de yazılır. redis.Nil'i gerçek bir hata gibi ele alıp kaydetmek ya da yeniden denemek yaygın bir hatadır: önbellekte ıskalama (cache miss) normal bir durumdur. Önbellek desenlerinin genel mantığı önbellek nedir yazısında.

Sonuçları Go tiplerine dönüştürme

go-redis komut sonuçlarını doğrudan Go değerlerine çevirebilir. Hash alanlarını bir yapıya (struct) okumak için alanlara redis:"alan-adı" biçiminde etiket koyar ve HGetAll() sonucu üzerinde Scan() çağırırsınız; eşleşen alanlar hedef tiplere çevrilir ve dönüşüm başarısız olursa hata döner. HMGet() ile yalnızca bazı alanları okuyup tarayabilirsiniz; dönmeyen alanlar Go'nun sıfır değerinde kalır. LRANGE gibi metin listesi döndüren komutlarda ScanSlice() sonucu tipli bir dilime çevirir.

Büyük metin değerlerinde her okumada yeni bir string oluşturmamak için go-redis v9.21.0 ve sonrasında tampon API'leri vardır: SetFromBuffer() bir []byte yazar, GetToBuffer() değeri sizin sağladığınız tampona okur. Belgeler bu API'nin üç sınırını belirtir: tamponun değerin tamamını alacak kadar büyük olması gerekir, yoksa Err() bir buffer too small hatası bildirir; başarısız bir okuma tampona kısmi veri yazmış olabileceği için GetToBuffer() otomatik yeniden denemeye katılmaz; SetFromBuffer() son kullanma süresi ayarlamaz, süre gerekiyorsa ayrıca Expire() çağırırsınız.

go-redis ile pipeline

Pipeline oluşturmak için Pipeline() çağırır, komutları normal metot adlarıyla eklersiniz. Komutlar tamponda bekler ve Exec() çağrıldığında gönderilir. Burada önemli fark şudur: pipeline'daki komutların dönüş değerleri ancak pipeline çalıştıktan sonra geçerli bir sonuç içerir ve hatalar tek tek komutlar yerine Exec() tarafından bildirilir. Sonuca Result() yerine Val() ile erişmeniz önerilir. Belgelerdeki özellik deposu örneği, birçok kullanıcı için HMGET komutunu tek gidiş dönüşte gönderir ve komut nesnelerini sonradan okumak üzere saklar:

go
pipe := fs.rdb.Pipeline()
cmds := make([]*redis.SliceCmd, len(entityIDs))
for i, id := range entityIDs {
    cmds[i] = pipe.HMGet(ctx, fs.KeyFor(id), fieldNames...)
}
if _, err := pipe.Exec(ctx); err != nil && !errors.Is(err, redis.Nil) {
    return nil, err
}
out := make(map[string]map[string]string, len(entityIDs))
for i, id := range entityIDs {
    values, _ := cmds[i].Result()
    row := make(map[string]string, len(fieldNames))
    for j, name := range fieldNames {
        if s, ok := values[j].(string); ok {
            row[name] = s
        }
    }
    out[id] = row
}

Örnekte Exec() hatası redis.Nil ise gerçek bir hata sayılmadığına dikkat edin; anahtarı ya da alanı olmayan kullanıcılar toplu okumayı başarısız saymamalıdır. Sonuçlar komut nesnelerinden, Exec() döndükten sonra okunur ve metin olmayan değerler (yani bulunmayan alanlar) satıra eklenmez. Pipeline'ı geri çağırmalı biçimde kurmak isterseniz Pipelined() kullanılır; komutları sizin verdiğiniz fonksiyon içinde ekler ve fonksiyon dönünce Exec() kendiliğinden çağrılır.

Belgeler Pipeline'ın transaction olmadığını da vurgular: komutlar tek gidiş dönüşte gönderilir ama MULTI/EXEC bloğu içinde çalışmaz. Her kullanıcının HSET ve EXPIRE çifti diğerlerinden bağımsızsa bu doğru seçimdir; çiftin ayrılamaz olması gerekiyorsa, örneğin iki komut arasında sunucu çökerse anahtarın süresiz kalması kabul edilemiyorsa, her kullanıcı için TxPipeline() ya da bir Lua betiği kullanılır.

TxPipeline ve Watch ile transaction

Transaction da aynı biçimde kurulur: TxPipeline() ile nesneyi oluşturur, komutları ekler ve Exec() ile çalıştırırsınız; sonuçlara iş bittikten sonra Val() ile erişilir. Geri çağırmalı karşılığı TxPipelined()'dır. İyimser kilit için ise Watch() metodu kullanılır. Watch() bir geri çağırma fonksiyonu ve ardından izlenecek anahtarları alır. Fonksiyonun içinde, transaction'dan önce salt okunur komutları çalıştırıp sonucunu hemen alabilir, transaction'ı ise fonksiyona verilen Tx nesnesi üzerinde TxPipeline() veya TxPipelined() ile başlatırsınız. İzlenen anahtar yazmadan önce değişirse transaction iptal edilir ve redis.TxFailedErr döner. Belgelerdeki önbellek örneği bunu bir döngüyle yeniden dener:

go
func (c *RedisCache) UpdateField(ctx context.Context, id, field, value string) (bool, error) {
    cacheKey := c.cacheKey(id)
    for {
        var ok bool
        err := c.client.Watch(ctx, func(tx *redis.Tx) error {
            exists, err := tx.Exists(ctx, cacheKey).Result()
            if err != nil {
                return err
            }
            if exists == 0 {
                ok = false
                return nil
            }
            _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
                pipe.HSet(ctx, cacheKey, field, value)
                pipe.Expire(ctx, cacheKey, c.ttl)
                return nil
            })
            ok = err == nil
            return err
        }, cacheKey)
        if errors.Is(err, redis.TxFailedErr) {
            continue
        }
        return ok, err
    }
}

Bu kod yalnızca önbellekte zaten bulunan bir kaydın tek alanını günceller; böylece Redis'te hiçbir zaman yarım bir kayıt oluşmaz. Döngü yalnızca TxFailedErr durumunda tekrarlanır, diğer hatalar çağırana döner. Transaction'ların sunucu tarafındaki davranışını MULTI, EXEC ve WATCH, pipelining'in genel mantığını Redis pipelining yazısında anlatıyoruz.

Bağlantı havuzu ve pipeline havuzu

Bağlantı havuzu ve pipeline havuzu: PoolSize, MinIdleConns, MaxActiveConns, PoolTimeout

go-redis bağlantıları sizin için bir havuzla yönetir; açık bağlantıları kendiniz saklayıp yeniden kullanmanız gerekmez. Belgelerdeki ayarlar ve varsayılan değerler şunlardır:

AlanGöreviVarsayılan
PoolSizeAna havuzun tuttuğu bağlantı sayısıGOMAXPROCS değerinin on katı
MinIdleConnsUygulama istemeden önce açılacak bağlantı sayısıÖnceden bağlantı açılmaz
MaxActiveConnsToplam bağlantı sayısı üst sınırıSınır yok
PoolTimeoutBir komutun boş bağlantı için bekleme süresiReadTimeout değerinden bir saniye fazla
PipelinePoolSizeAyrı pipeline havuzunun bağlantı sayısı (v9.23.0 ve sonrası)10

go-redis v9.23.0 ve sonrasında her istemci, pipeline'lar, transaction'lar ve otomatik pipelining için ayrı bir pipeline havuzu da oluşturur. Amaç, bir toplu iş patlamasının sıradan komutlarınızla aynı bağlantılar için yarışmasını önlemektir. Pipeline havuzu kullanılmadığında maliyeti yoktur, çünkü MinIdleConns değerinden bağımsız olarak önceden bağlantı açmaz. PipelinePoolSize değerini 0 yaparsanız varsayılan sayı kullanılır, -1 yaparsanız ayrı havuz kapanır ve pipeline'lar ana havuzdan bağlantı alır. Bütün pipeline bağlantıları meşgulse topluluk beklemez, ana havuzda hemen çalışır. Havuz durumunu şöyle görebilirsiniz:

go
stats := client.PoolStats()
fmt.Printf("Main pool connections: %d, hits: %d, misses: %d\n",
    stats.TotalConns, stats.Hits, stats.Misses)
if ps := stats.PipelineStats; ps != nil {
    fmt.Printf("Pipeline pool connections: %d, hits: %d, misses: %d\n",
        ps.TotalConns, ps.Hits, ps.Misses)
}

Belgelerin önerisi şudur: toplu iş patlamaları sırasında PipelineStats.TotalConns değeri PipelinePoolSize'da sabit kalırken ana havuzun TotalConns değeri büyüyorsa PipelinePoolSize'ı artırın. Ayrı havuz kapalıyken PipelineStats değeri nil olur.

Yeniden deneme, zaman aşımı ve sağlık kontrolü

go-redis başarısız bağlantıları ve komutları kendiliğinden yeniden dener. Varsayılan deneme sayısı üçtür ve MaxRetries ile değiştirilir. Denemeler arasındaki bekleme her seferinde artar; başlangıç beklemesi MinRetryBackoff (varsayılan 8 milisaniye), en uzun bekleme MaxRetryBackoff (varsayılan 512 milisaniye) ile ayarlanır. Zaman aşımlarında DialTimeout bağlantı kurma, ReadTimeout ve WriteTimeout okuma ve yazma süresini belirler; varsayılanlar bağlantı için beş, okuma ve yazma için üç saniyedir:

go
client := redis.NewClient(&redis.Options{
    MinRetryBackoff: 10 * time.Millisecond,
    MaxRetryBackoff: 100 * time.Millisecond,
    MaxRetries:      5,
    DialTimeout:     10 * time.Second,
    ReadTimeout:     5 * time.Second,
    WriteTimeout:    5 * time.Second,
})

// Sağlık kontrolü
err := client.Ping(ctx).Err()
if err != nil {
  // Report failed health check.
}

Belgeler zaman aşımında dengeyi hatırlatır: çok kısa süreler biraz daha beklense başarılı olacak komutların gereksiz yeniden denenmesine, çok uzun süreler ise hiç gelmeyecek yanıt için uygulamanın askıda kalmasına yol açar. Sunucuya sürekli erişmeyen uygulamalarda birkaç saniyede bir PING ile sağlık kontrolü yapmak sorunları kullanıcılar fark etmeden yakalamanızı sağlar. go-redis OpenTelemetry de destekler; deneme sayılarını ve zaman aşımı sıklığını izleyip bu ayarları gerçek veriye göre seçebilirsiniz.

Otomatik pipelining

Otomatik pipelining: Kuyruğa giren bir komutun bağlamı (context) artık dikkate alınmaz, çünkü..., BLPOP ve WAIT gibi engelleyen komutlar hiçbir zaman topluluğa alınmaz...

Elle pipeline kurmak, hangi komutları toplu göndereceğinizi önceden bilmenizi gerektirir. go-redis v9.22.0 ve sonrasında gelen otomatik pipelining bu zorunluluğu kaldırır: çok sayıda goroutine eşzamanlı komut gönderdiğinde go-redis onları sizin yerinize derin pipeline'larda birleştirir. Belgeler bunun yüksek eşzamanlılıkta yararlı olduğunu, düşük eşzamanlılıkta düz istemcinin daha basit ve aynı hızda olduğunu, elle yazılmış bir pipeline'ın ise genellikle otomatik olandan hızlı olduğunu söyler.

İki yöntem vardır. AutoPipeline() engelleyen (blocking) yöntemdir ve normal istemcinin yerine doğrudan geçer: her çağrı yürütülene kadar bekler ve kendi değeriyle hatasını döndürür, goroutine başına sıra korunur. AsyncAutoPipeline() en yüksek verimi sunar: çağrılar hemen döner, sonucu Val(), Result() veya Err() ile okumak topluluk çalışana kadar bekletir.

go
ap, err := rdb.AutoPipeline()
if err != nil { // only returned for invalid AutoPipelineOptions
    log.Fatal(err)
}
defer ap.Close()

var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func(i int) {
        defer wg.Done()
        key := fmt.Sprintf("key:%d", i)
        if err := ap.Set(ctx, key, i, 0).Err(); err != nil { // blocks until executed
            log.Printf("set %s: %v", key, err)
        }
    }(i)
}
wg.Wait()

Belgelerdeki sınırlamalar, bu özelliği kullanmadan önce bilinmesi gerekenlerdir:

  • Kuyruğa giren bir komutun bağlamı (context) artık dikkate alınmaz, çünkü topluluk otomatik pipeline'ın kendi bağlamında çalışır. Komut başına süre sınırı gerekiyorsa düz istemci kullanın.
  • BLPOP ve WAIT gibi engelleyen komutlar hiçbir zaman topluluğa alınmaz, doğrudan sizin bağlamınızda çalışır.
  • Genel Do, DoRaw ve DoRawWriteTo metotları pipeline dışında, normal bir bağlantıda çalışır; tipli metotları (ap.Set(), ap.Get()) tercih edin.
  • Bağlantı koparsa topluluk istemcinin MaxRetries değerine kadar bütün olarak yeniden denenir; bu yüzden idempotent olmayan komutlar iki kez çalışabilir. Asla yeniden gönderilmemesi gereken komutlar için MaxRetries: -1 verin ya da düz istemci kullanın.
  • Her istemcide engelleyen ve asenkron yöntem için birer tekil (singleton) otomatik pipeline bulunur; seçenekler yalnızca onu oluşturan ilk çağrıda geçerli olur.

Cluster, Sentinel ve TLS

Cluster için NewClusterClient() kullanılır ve Addrs alanında bir veya daha fazla cluster adresi verilir; komutları gecikmeye göre ya da rastgele yönlendirmek için RouteByLatency veya RouteRandomly açılabilir. Belgelere göre bir cluster'da tek bir Pipeline.Exec tek bir parçaya bağlıdır; farklı parçalara düşen anahtarlar için bunu hesaba katmanız gerekir. Hash slot kavramı Redis Cluster ve CROSSSLOT yazısında. Sentinel için NewFailoverClient() vardır: MasterName izlenen birincilin adı, SentinelAddrs Sentinel adresleridir ve istemci yük devretmeden sonra yeni birincile geçer. Sentinel şifresi SentinelPassword, veri düğümlerinin kimlik bilgileri Username ve Password alanlarında verilir. ReplicaOnly açıldığında yazmalar dahil her komut rastgele bir kopyaya gider ve yazmalar başarısız olur; bu seçeneği yalnızca okuma yapan bir istemci için kullanın. TLS için TLSConfig alanına bir tls.Config verilir; belgelerdeki örnek MinVersion: tls.VersionTLS12, istemci sertifikası ve CA havuzu kullanır.

İstemci tarafı önbellek go-redis'te deneysel bir özelliktir ve API'si küçük bir sürümde değişebilir. go-redis v9.22.0 veya üstünü, en geniş uyumluluk için Redis 7.4 veya üstünü ve Protocol: 3 ayarını gerektirir; RESP2 bağlantısında hiçbir şey yapmaz, yalnızca tekil istemcilerde ve 0 numaralı mantıksal veritabanında çalışır. Ayrıntı client-side caching yazısında.

Sık yapılan hatalar

  • Result() dönen hatayı kontrol etmemek. Belgeler hatanın her zaman nil olup olmadığına bakılmasını ister.
  • redis.Nil'i gerçek hata saymak. Anahtarın olmaması çoğu zaman normal bir durumdur; ayrı ele alın.
  • Pipeline komutlarının sonucunu Exec() öncesinde okumak. Sonuçlar ancak pipeline çalıştıktan sonra geçerlidir.
  • Pipeline'ın atomik olduğunu varsaymak. Atomiklik için TxPipeline() veya TxPipelined() kullanın.
  • URL'ye başka istemcilerin parametrelerini eklemek. go-redis tanımadığı parametrede ParseURL() hatası verir.
  • Otomatik pipeline'da komut başına bağlam beklemek. Kuyruğa giren komutlarda bağlam dikkate alınmaz.

Sık Sorulan Sorular

Get() anahtar yoksa ne döndürür?

Hata değeri olarak redis.Nil döner. err == redis.Nil ya da errors.Is(err, redis.Nil) ile kontrol edip varsayılan değere veya veritabanına dönebilirsiniz.

Pipeline ile TxPipeline arasındaki fark nedir?

Pipeline() komutları tek gidiş dönüşte gönderir ama MULTI/EXEC kullanmaz. TxPipeline() aynı topluluğu bir transaction olarak, başka istemcilerin komutları araya girmeden çalıştırır.

Havuz boyutunu nasıl seçmeliyim?

Varsayılan PoolSize, GOMAXPROCS değerinin on katıdır. PoolStats() ile havuz istatistiklerini izleyip ihtiyaca göre PoolSize, MaxActiveConns ve PipelinePoolSize değerlerini ayarlayın.

Otomatik pipelining her zaman daha mı hızlı?

Hayır. Belgelere göre yüksek eşzamanlılıkta yararlıdır; düşük eşzamanlılıkta düz istemci aynı hızdadır ve elle yazılmış bir pipeline genellikle otomatik olandan hızlıdır.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Redis öğrenme rehberi · Redis bağlantı URL ve connection pool · MULTI, EXEC ve WATCH · Redis pipelining · Redis Cluster ve CROSSSLOT · Go öğrenme rehberi