Redis #

Redis (Remote Dictionary Server) adalah in-memory data store yang bisa digunakan sebagai database, cache, message broker, dan session store. Kecepatannya luar biasa — operasi baca/tulis bisa mencapai ratusan ribu per detik karena semua data disimpan di memori. Go menggunakan library github.com/redis/go-redis/v9 yang merupakan client Redis paling lengkap dan aktif dikembangkan.

Instalasi #

go get github.com/redis/go-redis/v9

Koneksi ke Redis #

import "github.com/redis/go-redis/v9"

func newRedisClient(addr, password string, db int) *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr:         addr,      // "localhost:6379"
        Password:     password,  // "" jika tidak ada password
        DB:           db,        // database index, default 0

        // Connection pool
        PoolSize:     25,
        MinIdleConns: 5,
        PoolTimeout:  30 * time.Second,

        // Timeout
        DialTimeout:  5 * time.Second,
        ReadTimeout:  3 * time.Second,
        WriteTimeout: 3 * time.Second,

        // Retry
        MaxRetries:      3,
        MinRetryBackoff: 8 * time.Millisecond,
        MaxRetryBackoff: 512 * time.Millisecond,
    })
}

// Redis Cluster
func newClusterClient(addrs []string) *redis.ClusterClient {
    return redis.NewClusterClient(&redis.ClusterOptions{
        Addrs:    addrs,
        PoolSize: 10,
    })
}

// Redis Sentinel (High Availability)
func newSentinelClient(masterName string, sentinels []string) *redis.Client {
    return redis.NewFailoverClient(&redis.FailoverOptions{
        MasterName:    masterName,
        SentinelAddrs: sentinels,
    })
}

func main() {
    rdb := newRedisClient("localhost:6379", "", 0)
    defer rdb.Close()

    ctx := context.Background()
    if err := rdb.Ping(ctx).Err(); err != nil {
        log.Fatal("Redis ping:", err)
    }
    fmt.Println("✓ Terhubung ke Redis")
}

String — Tipe Data Paling Dasar #

ctx := context.Background()

// SET dengan TTL
err := rdb.Set(ctx, "session:abc123", "user:42", 24*time.Hour).Err()

// GET
val, err := rdb.Get(ctx, "session:abc123").Result()
if err == redis.Nil {
    fmt.Println("Key tidak ada")
} else if err != nil {
    log.Fatal(err)
} else {
    fmt.Println("Value:", val)
}

// SETNX — set hanya jika key belum ada
ok, err := rdb.SetNX(ctx, "lock:resource", "worker-1", 30*time.Second).Result()
if ok {
    fmt.Println("Lock acquired")
}

// GETSET — set dan kembalikan nilai lama
old, err := rdb.GetSet(ctx, "counter", "0").Result()

// INCR / DECR — atomic increment/decrement
newVal, err := rdb.Incr(ctx, "page_views").Result()
rdb.IncrBy(ctx, "score", 10)
rdb.Decr(ctx, "stock:product:1")
rdb.DecrBy(ctx, "balance", 50000)

// MSET / MGET — banyak key sekaligus
rdb.MSet(ctx, "key1", "val1", "key2", "val2", "key3", "val3")
vals, err := rdb.MGet(ctx, "key1", "key2", "key3").Result()

// EXPIRE — set TTL pada key yang sudah ada
rdb.Expire(ctx, "session:abc123", 1*time.Hour)

// TTL — cek sisa waktu hidup
ttl, err := rdb.TTL(ctx, "session:abc123").Result()
fmt.Printf("TTL: %v\n", ttl) // -1 = no expiry, -2 = key tidak ada

// EXISTS dan DEL
exists, _ := rdb.Exists(ctx, "key1").Result()
rdb.Del(ctx, "key1", "key2", "key3")

Hash — Map dalam Key #

// HSET — set satu atau banyak field
rdb.HSet(ctx, "user:42", map[string]interface{}{
    "name":  "Budi Santoso",
    "email": "[email protected]",
    "age":   28,
    "role":  "admin",
})

// HGET — ambil satu field
name, _ := rdb.HGet(ctx, "user:42", "name").Result()

// HMGET — ambil banyak field
vals, _ := rdb.HMGet(ctx, "user:42", "name", "email", "role").Result()

// HGETALL — ambil semua field sebagai map
fields, _ := rdb.HGetAll(ctx, "user:42").Result()
fmt.Println(fields) // map[age:28 email:[email protected] ...]

// Scan ke struct
type User struct {
    Name  string `redis:"name"`
    Email string `redis:"email"`
    Age   int    `redis:"age"`
    Role  string `redis:"role"`
}
var user User
if err := rdb.HGetAll(ctx, "user:42").Scan(&user); err != nil {
    log.Fatal(err)
}
fmt.Printf("%+v\n", user)

// HINCRBY — increment field numerik
rdb.HIncrBy(ctx, "user:42", "login_count", 1)

// HDEL — hapus field
rdb.HDel(ctx, "user:42", "role")

// HEXISTS
exists, _ := rdb.HExists(ctx, "user:42", "email").Result()

List — Antrian dan Stack #

// LPUSH / RPUSH — tambah ke kiri/kanan
rdb.LPush(ctx, "queue:jobs", "job-1", "job-2", "job-3")
rdb.RPush(ctx, "log:events", "event-a", "event-b")

// LPOP / RPOP — ambil dari kiri/kanan (non-blocking)
job, err := rdb.LPop(ctx, "queue:jobs").Result()

// BLPOP — blocking pop (tunggu hingga ada item)
result, err := rdb.BLPop(ctx, 5*time.Second, "queue:jobs").Result()
// result[0] = nama key, result[1] = value

// LRANGE — ambil range
items, _ := rdb.LRange(ctx, "log:events", 0, -1).Result() // semua item
recent, _ := rdb.LRange(ctx, "log:events", 0, 9).Result() // 10 terbaru

// LLEN — panjang list
length, _ := rdb.LLen(ctx, "queue:jobs").Result()

// LTRIM — pertahankan hanya N item (sliding window log)
rdb.LTrim(ctx, "log:events", 0, 99) // pertahankan 100 item terbaru

Set — Koleksi Unik #

// SADD — tambah member
rdb.SAdd(ctx, "online_users", "user:42", "user:99", "user:17")

// SISMEMBER — cek keanggotaan
isMember, _ := rdb.SIsMember(ctx, "online_users", "user:42").Result()

// SMEMBERS — semua member
members, _ := rdb.SMembers(ctx, "online_users").Result()

// SCARD — jumlah member
count, _ := rdb.SCard(ctx, "online_users").Result()

// SREM — hapus member
rdb.SRem(ctx, "online_users", "user:42")

// Operasi set
rdb.SAdd(ctx, "tags:post:1", "go", "backend", "api")
rdb.SAdd(ctx, "tags:post:2", "go", "concurrency", "goroutine")

// SINTER — irisan
common, _ := rdb.SInter(ctx, "tags:post:1", "tags:post:2").Result()
// ["go"]

// SUNION — gabungan
all, _ := rdb.SUnion(ctx, "tags:post:1", "tags:post:2").Result()

// SDIFF — perbedaan
diff, _ := rdb.SDiff(ctx, "tags:post:1", "tags:post:2").Result()

Sorted Set — Set Berurutan dengan Skor #

// ZADD — tambah member dengan skor
rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 9500, Member: "player:alice"})
rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 8200, Member: "player:budi"})
rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 9800, Member: "player:charlie"})

// ZRANGE — ambil berdasarkan rank (ascending)
top, _ := rdb.ZRange(ctx, "leaderboard", 0, 2).Result()

// ZREVRANGE — descending (skor tertinggi dulu)
topPlayers, _ := rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, 9).Result()
for i, p := range topPlayers {
    fmt.Printf("%d. %s: %.0f\n", i+1, p.Member, p.Score)
}

// ZRANK — posisi member (0-indexed)
rank, _ := rdb.ZRevRank(ctx, "leaderboard", "player:alice").Result()
fmt.Printf("Alice rank: %d\n", rank+1)

// ZINCRBY — tambah skor
rdb.ZIncrBy(ctx, "leaderboard", 300, "player:budi")

// ZRANGEBYSCORE — filter berdasarkan range skor
high, _ := rdb.ZRangeByScore(ctx, "leaderboard", &redis.ZRangeBy{
    Min: "9000", Max: "+inf",
}).Result()

Pipeline — Batch Command #

Pipeline mengirim banyak command sekaligus, mengurangi round-trip overhead:

// Pipeline — tidak ada transaksi, command dikirim sekaligus
pipe := rdb.Pipeline()
pipe.Set(ctx, "key1", "val1", time.Hour)
pipe.Set(ctx, "key2", "val2", time.Hour)
pipe.Incr(ctx, "counter")
pipe.HSet(ctx, "hash", "field", "value")

results, err := pipe.Exec(ctx)
if err != nil {
    log.Fatal(err)
}
for _, result := range results {
    if result.Err() != nil {
        log.Printf("Command error: %v", result.Err())
    }
}

// Pipelining dengan return values
var (
    get1 *redis.StringCmd
    get2 *redis.StringCmd
)
_, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
    get1 = pipe.Get(ctx, "key1")
    get2 = pipe.Get(ctx, "key2")
    return nil
})
fmt.Println(get1.Val(), get2.Val())

Transaksi dengan WATCH (Optimistic Lock) #

// WATCH + MULTI/EXEC — transaksi optimistic
func transferPoints(ctx context.Context, rdb *redis.Client, from, to string, amount int64) error {
    return rdb.Watch(ctx, func(tx *redis.Tx) error {
        // Baca saldo
        fromBal, err := tx.Get(ctx, "points:"+from).Int64()
        if err != nil {
            return err
        }
        if fromBal < amount {
            return errors.New("saldo tidak mencukupi")
        }

        // Eksekusi dalam transaksi (MULTI/EXEC)
        _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
            pipe.DecrBy(ctx, "points:"+from, amount)
            pipe.IncrBy(ctx, "points:"+to, amount)
            return nil
        })
        return err
        // Jika ada perubahan pada "points:from" atau "points:to" sejak WATCH,
        // transaksi gagal dan akan di-retry otomatis
    }, "points:"+from, "points:"+to)
}

Distributed Lock #

// Distributed lock — mencegah race condition di sistem terdistribusi
func acquireLock(ctx context.Context, rdb *redis.Client, key string, ttl time.Duration) (string, error) {
    token := uuid.New().String()

    // SET NX — hanya set jika belum ada (atomic)
    ok, err := rdb.SetNX(ctx, "lock:"+key, token, ttl).Result()
    if err != nil {
        return "", err
    }
    if !ok {
        return "", errors.New("lock sudah dipegang proses lain")
    }
    return token, nil
}

func releaseLock(ctx context.Context, rdb *redis.Client, key, token string) error {
    // Gunakan Lua script untuk release yang aman (atomic check & delete)
    script := redis.NewScript(`
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
    `)
    result, err := script.Run(ctx, rdb, []string{"lock:" + key}, token).Int()
    if err != nil {
        return err
    }
    if result == 0 {
        return errors.New("lock sudah expired atau dipegang proses lain")
    }
    return nil
}

// Penggunaan
func processWithLock(ctx context.Context, rdb *redis.Client, resourceID string) error {
    token, err := acquireLock(ctx, rdb, resourceID, 30*time.Second)
    if err != nil {
        return fmt.Errorf("tidak bisa acquire lock: %w", err)
    }
    defer releaseLock(ctx, rdb, resourceID, token)

    // Proses dengan aman — tidak ada proses lain yang bisa masuk
    return doWork(resourceID)
}

Pub/Sub #

// Subscribe ke channel
func subscribe(ctx context.Context, rdb *redis.Client, channels ...string) {
    pubsub := rdb.Subscribe(ctx, channels...)
    defer pubsub.Close()

    ch := pubsub.Channel()
    for msg := range ch {
        fmt.Printf("Channel: %s, Message: %s\n", msg.Channel, msg.Payload)
    }
}

// Publish ke channel
func publish(ctx context.Context, rdb *redis.Client, channel, message string) error {
    return rdb.Publish(ctx, channel, message).Err()
}

// Pattern subscribe
func psubscribe(ctx context.Context, rdb *redis.Client) {
    pubsub := rdb.PSubscribe(ctx, "order.*")
    defer pubsub.Close()

    for msg := range pubsub.Channel() {
        fmt.Printf("Pattern: %s, Channel: %s, Msg: %s\n",
            msg.Pattern, msg.Channel, msg.Payload)
    }
}

Contoh Program Lengkap — Caching Layer #

package main

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "time"

    "github.com/redis/go-redis/v9"
)

var ErrCacheMiss = errors.New("cache miss")

type Product struct {
    ID        int     `json:"id"`
    Name      string  `json:"name"`
    Price     float64 `json:"price"`
    Stock     int     `json:"stock"`
    Category  string  `json:"category"`
}

// Cache — generic caching layer
type Cache struct {
    rdb    *redis.Client
    prefix string
    ttl    time.Duration
}

func NewCache(rdb *redis.Client, prefix string, ttl time.Duration) *Cache {
    return &Cache{rdb: rdb, prefix: prefix, ttl: ttl}
}

func (c *Cache) key(id string) string {
    return c.prefix + ":" + id
}

func (c *Cache) Set(ctx context.Context, id string, value interface{}) error {
    data, err := json.Marshal(value)
    if err != nil {
        return err
    }
    return c.rdb.Set(ctx, c.key(id), data, c.ttl).Err()
}

func (c *Cache) Get(ctx context.Context, id string, dest interface{}) error {
    data, err := c.rdb.Get(ctx, c.key(id)).Bytes()
    if errors.Is(err, redis.Nil) {
        return ErrCacheMiss
    }
    if err != nil {
        return err
    }
    return json.Unmarshal(data, dest)
}

func (c *Cache) Delete(ctx context.Context, id string) error {
    return c.rdb.Del(ctx, c.key(id)).Err()
}

func (c *Cache) DeletePattern(ctx context.Context, pattern string) error {
    keys, err := c.rdb.Keys(ctx, c.prefix+":"+pattern).Result()
    if err != nil || len(keys) == 0 {
        return err
    }
    return c.rdb.Del(ctx, keys...).Err()
}

// ProductService dengan caching
type ProductService struct {
    cache *Cache
    // db   *sql.DB  // dalam produksi, ini adalah database nyata
}

func (s *ProductService) GetProduct(ctx context.Context, id int) (*Product, error) {
    key := fmt.Sprintf("%d", id)
    var product Product

    // Coba dari cache dulu
    if err := s.cache.Get(ctx, key, &product); err == nil {
        fmt.Printf("  [CACHE HIT] product:%d\n", id)
        return &product, nil
    }

    fmt.Printf("  [CACHE MISS] product:%d — ambil dari DB\n", id)
    // Simulasi ambil dari database
    product = Product{
        ID: id, Name: fmt.Sprintf("Produk #%d", id),
        Price: float64(id) * 10000, Stock: 50, Category: "elektronik",
    }
    time.Sleep(50 * time.Millisecond) // simulasi query DB

    // Simpan ke cache
    s.cache.Set(ctx, key, product)

    return &product, nil
}

func (s *ProductService) UpdateProduct(ctx context.Context, p *Product) error {
    fmt.Printf("  [DB UPDATE] product:%d\n", p.ID)
    time.Sleep(30 * time.Millisecond)

    // Invalidate cache
    s.cache.Delete(ctx, fmt.Sprintf("%d", p.ID))
    fmt.Printf("  [CACHE INVALIDATED] product:%d\n", p.ID)
    return nil
}

// Rate Limiter menggunakan Redis
type RateLimiter struct {
    rdb *redis.Client
}

func (rl *RateLimiter) Allow(ctx context.Context, key string, limit int, window time.Duration) (bool, error) {
    pipe := rl.rdb.Pipeline()
    incr := pipe.Incr(ctx, "ratelimit:"+key)
    pipe.Expire(ctx, "ratelimit:"+key, window)

    if _, err := pipe.Exec(ctx); err != nil {
        return false, err
    }

    count := incr.Val()
    if count > int64(limit) {
        return false, nil
    }
    return true, nil
}

func main() {
    ctx := context.Background()

    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
        DB:   0,
    })
    defer rdb.Close()

    if err := rdb.Ping(ctx).Err(); err != nil {
        log.Fatal("Redis:", err)
    }
    fmt.Println("✓ Terhubung ke Redis\n")

    // Demo caching
    cache := NewCache(rdb, "product", 5*time.Minute)
    svc := &ProductService{cache: cache}

    fmt.Println("=== Cache Demo ===")
    for i := 0; i < 3; i++ {
        fmt.Printf("\nRequest ke-%d untuk product:1:\n", i+1)
        p, err := svc.GetProduct(ctx, 1)
        if err != nil {
            log.Println(err)
        } else {
            fmt.Printf("  Hasil: %s (Rp%.0f)\n", p.Name, p.Price)
        }
    }

    // Update dan invalidasi cache
    fmt.Println("\n=== Update Produk ===")
    svc.UpdateProduct(ctx, &Product{ID: 1, Name: "Produk Update", Price: 99000})

    fmt.Println("\nRequest setelah update:")
    p, _ := svc.GetProduct(ctx, 1)
    fmt.Printf("  Hasil: %s\n", p.Name)

    // Demo Sorted Set — Leaderboard
    fmt.Println("\n=== Leaderboard ===")
    players := []redis.Z{
        {Score: 9800, Member: "charlie"},
        {Score: 9500, Member: "alice"},
        {Score: 8200, Member: "budi"},
        {Score: 9100, Member: "diana"},
    }
    rdb.ZAdd(ctx, "demo:leaderboard", players...)

    top, _ := rdb.ZRevRangeWithScores(ctx, "demo:leaderboard", 0, 2).Result()
    fmt.Println("Top 3:")
    for i, p := range top {
        fmt.Printf("  %d. %-10s %.0f poin\n", i+1, p.Member, p.Score)
    }

    // Demo Rate Limiter
    fmt.Println("\n=== Rate Limiter (5 req/10 detik) ===")
    rl := &RateLimiter{rdb: rdb}
    rdb.Del(ctx, "ratelimit:user:42")

    for i := 1; i <= 7; i++ {
        allowed, _ := rl.Allow(ctx, "user:42", 5, 10*time.Second)
        status := "✓ ALLOWED"
        if !allowed {
            status = "✗ BLOCKED"
        }
        fmt.Printf("  Request %d: %s\n", i, status)
    }

    // Cleanup
    rdb.Del(ctx, "demo:leaderboard")
}

Ringkasan #

  • go-redis/v9 adalah client Redis terlengkap untuk Go — mendukung Cluster, Sentinel, dan pipeline.
  • redis.Nil bukan error kritis — cek dengan errors.Is(err, redis.Nil) untuk cache miss.
  • Hash (HSet, HGetAll, HGetAll.Scan) untuk menyimpan struct secara efisien per-field.
  • Sorted Set untuk leaderboard, rate limiting berbasis waktu, dan antrian berprioritas.
  • Pipeline untuk batch command — kurangi round-trip, tingkatkan throughput.
  • WATCH + TxPipelined untuk transaksi optimistic — retry saat ada konflik.
  • Distributed lock dengan SetNX dan Lua script untuk release yang aman secara atomik.
  • Pub/Sub untuk real-time messaging ringan — bukan pengganti Kafka untuk streaming besar.
  • Expire dan TTL untuk manajemen lifetime cache — selalu set TTL untuk mencegah memory penuh.
  • BLPOP untuk reliable job queue — blocking pop yang efisien tanpa polling loop.

← Sebelumnya: Google Pub/Sub   Berikutnya: Memcached →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact