Memcached #

Memcached adalah distributed memory caching system yang sangat sederhana dan sangat cepat. Dibandingkan Redis yang kaya fitur (persistence, data structures, pub/sub), Memcached jauh lebih minimalis — hanya menyimpan key-value string dengan expiration time. Kesederhanaan ini adalah kekuatannya: Memcached sangat ringan, mudah di-scale horizontal, dan performa read/write-nya konsisten bahkan untuk beban sangat tinggi. Go menggunakan library github.com/bradfitz/gomemcache/memcache.

Kapan Memilih Memcached vs Redis #

Pilih MEMCACHED jika:
  ✓ Hanya butuh simple key-value cache
  ✓ Data bisa dibuang (eviction) tanpa masalah
  ✓ Scale horizontal mudah dengan banyak node
  ✓ Memory footprint harus minimal
  ✓ Tim sudah familiar dengan Memcached

Pilih REDIS jika:
  ✓ Butuh persistence (data tidak hilang saat restart)
  ✓ Butuh data structures (list, set, sorted set, hash)
  ✓ Butuh pub/sub messaging
  ✓ Butuh distributed lock yang robust
  ✓ Butuh Lua scripting
  ✓ Butuh replication dan clustering bawaan

Instalasi #

go get github.com/bradfitz/gomemcache/memcache

Koneksi ke Memcached #

import "github.com/bradfitz/gomemcache/memcache"

func newMemcacheClient(servers ...string) *memcache.Client {
    mc := memcache.New(servers...)

    // Timeout settings
    mc.Timeout = 100 * time.Millisecond

    // Jumlah koneksi idle per server
    mc.MaxIdleConns = 100

    return mc
}

func main() {
    // Single server
    mc := newMemcacheClient("localhost:11211")

    // Multi-server (client otomatis menggunakan consistent hashing)
    mcCluster := newMemcacheClient(
        "cache-1:11211",
        "cache-2:11211",
        "cache-3:11211",
    )
    _ = mcCluster

    // Test koneksi
    if err := mc.Ping(); err != nil {
        log.Fatal("Memcached ping:", err)
    }
    fmt.Println("✓ Terhubung ke Memcached")
}

Operasi Dasar #

Set — Menyimpan Item #

// Set dengan expiration (dalam detik, 0 = tidak expire)
err := mc.Set(&memcache.Item{
    Key:        "product:1",
    Value:      []byte(`{"id":1,"name":"Laptop","price":15000000}`),
    Expiration: 3600, // 1 jam
})

// Contoh helper untuk marshal JSON
func setJSON(mc *memcache.Client, key string, value interface{}, expiry int32) error {
    data, err := json.Marshal(value)
    if err != nil {
        return err
    }
    return mc.Set(&memcache.Item{
        Key:        key,
        Value:      data,
        Expiration: expiry,
    })
}

Get — Membaca Item #

item, err := mc.Get("product:1")
if err == memcache.ErrCacheMiss {
    fmt.Println("Cache miss — ambil dari database")
    // ambil dari DB, lalu set ke cache
} else if err != nil {
    log.Fatal("Get error:", err)
} else {
    fmt.Println("Cache hit:", string(item.Value))
}

// Helper untuk unmarshal JSON
func getJSON(mc *memcache.Client, key string, dest interface{}) error {
    item, err := mc.Get(key)
    if err != nil {
        return err  // termasuk ErrCacheMiss
    }
    return json.Unmarshal(item.Value, dest)
}

// GetMulti — ambil banyak key sekaligus (satu round-trip)
items, err := mc.GetMulti([]string{"product:1", "product:2", "product:3"})
if err != nil {
    log.Fatal(err)
}
for key, item := range items {
    fmt.Printf("%s: %s\n", key, string(item.Value))
}

Add dan Replace #

// Add — set hanya jika key BELUM ada (error jika sudah ada)
err := mc.Add(&memcache.Item{
    Key:        "lock:resource",
    Value:      []byte("worker-1"),
    Expiration: 30,
})
if err == memcache.ErrNotStored {
    fmt.Println("Lock sudah dipegang proses lain")
}

// Replace — set hanya jika key SUDAH ada (error jika belum ada)
err = mc.Replace(&memcache.Item{
    Key:        "product:1",
    Value:      []byte(`{"updated":true}`),
    Expiration: 3600,
})

Delete #

// Hapus satu key
err := mc.Delete("product:1")
if err == memcache.ErrCacheMiss {
    fmt.Println("Key tidak ada, tidak perlu dihapus")
}

// DeleteAll — flush semua cache (HATI-HATI di production!)
err = mc.DeleteAll()

Increment dan Decrement #

// INCR — increment atomic (nilai harus berupa angka dalam string)
mc.Set(&memcache.Item{Key: "counter", Value: []byte("0"), Expiration: 3600})

newVal, err := mc.Increment("counter", 1)
fmt.Println("Counter:", newVal) // 1

mc.Increment("counter", 5)  // tambah 5

// DECR
newVal, err = mc.Decrement("counter", 2)
fmt.Println("Counter setelah decr:", newVal) // 4

CAS — Check-And-Set (Optimistic Lock) #

CAS mencegah race condition saat update — update hanya berhasil jika nilai belum berubah sejak dibaca:

func updateWithCAS(mc *memcache.Client, key string, updateFn func([]byte) []byte) error {
    for retries := 0; retries < 3; retries++ {
        // Get dengan CAS token
        item, err := mc.Gets(key)  // Gets (bukan Get) mengembalikan CAS token
        if err == memcache.ErrCacheMiss {
            return fmt.Errorf("key tidak ditemukan: %s", key)
        }
        if err != nil {
            return err
        }

        // Modifikasi nilai
        newValue := updateFn(item.Value)

        // CAS — update hanya jika nilai belum berubah sejak Gets
        item.Value = newValue
        err = mc.CompareAndSwap(item)
        if err == nil {
            return nil  // berhasil
        }
        if err == memcache.ErrCASConflict {
            // Nilai berubah sejak dibaca — coba lagi
            log.Printf("CAS conflict, retry %d", retries+1)
            time.Sleep(time.Duration(retries+1) * 10 * time.Millisecond)
            continue
        }
        return err
    }
    return errors.New("terlalu banyak CAS conflict")
}

// Contoh penggunaan CAS untuk update counter produk dilihat
func incrementViewCount(mc *memcache.Client, productID int) error {
    key := fmt.Sprintf("views:product:%d", productID)

    return updateWithCAS(mc, key, func(current []byte) []byte {
        count := 0
        fmt.Sscanf(string(current), "%d", &count)
        return []byte(fmt.Sprintf("%d", count+1))
    })
}

Serialisasi Struct dengan Compression #

Untuk menyimpan struct kompleks dengan compression (menghemat memory):

import (
    "bytes"
    "compress/gzip"
    "encoding/gob"
)

// Encode struct ke gob + gzip
func encodeCompressed(v interface{}) ([]byte, error) {
    var buf bytes.Buffer
    gz := gzip.NewWriter(&buf)

    enc := gob.NewEncoder(gz)
    if err := enc.Encode(v); err != nil {
        return nil, err
    }
    if err := gz.Close(); err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}

// Decode gzip + gob ke struct
func decodeCompressed(data []byte, v interface{}) error {
    gz, err := gzip.NewReader(bytes.NewReader(data))
    if err != nil {
        return err
    }
    defer gz.Close()
    return gob.NewDecoder(gz).Decode(v)
}

type ProductDetail struct {
    ID          int
    Name        string
    Description string  // bisa sangat panjang
    Images      []string
    Specs       map[string]string
    Reviews     []Review
}

func cacheProductDetail(mc *memcache.Client, p ProductDetail) error {
    data, err := encodeCompressed(p)
    if err != nil {
        return err
    }

    return mc.Set(&memcache.Item{
        Key:        fmt.Sprintf("product_detail:%d", p.ID),
        Value:      data,
        Expiration: 1800, // 30 menit
    })
}

func getProductDetail(mc *memcache.Client, id int) (*ProductDetail, error) {
    item, err := mc.Get(fmt.Sprintf("product_detail:%d", id))
    if err != nil {
        return nil, err
    }

    var p ProductDetail
    if err := decodeCompressed(item.Value, &p); err != nil {
        return nil, err
    }
    return &p, nil
}

Cache Aside Pattern #

Cache Aside adalah pola caching yang paling umum — aplikasi mengelola cache secara manual:

type ProductCache struct {
    mc  *memcache.Client
    ttl int32
}

func NewProductCache(mc *memcache.Client, ttl int32) *ProductCache {
    return &ProductCache{mc: mc, ttl: ttl}
}

func (c *ProductCache) Get(id int) (*Product, error) {
    key := fmt.Sprintf("product:%d", id)

    var p Product
    if err := getJSON(c.mc, key, &p); err == nil {
        return &p, nil // cache hit
    }
    return nil, memcache.ErrCacheMiss
}

func (c *ProductCache) Set(p *Product) error {
    return setJSON(c.mc, fmt.Sprintf("product:%d", p.ID), p, c.ttl)
}

func (c *ProductCache) Invalidate(id int) {
    c.mc.Delete(fmt.Sprintf("product:%d", id))
}

// Service yang menggunakan cache
type ProductService struct {
    cache *ProductCache
    db    *sql.DB
}

func (s *ProductService) GetProduct(ctx context.Context, id int) (*Product, error) {
    // 1. Cek cache
    if p, err := s.cache.Get(id); err == nil {
        return p, nil
    }

    // 2. Cache miss — ambil dari database
    var p Product
    err := s.db.QueryRowContext(ctx,
        "SELECT id, name, price, stock, category FROM products WHERE id = ?", id,
    ).Scan(&p.ID, &p.Name, &p.Price, &p.Stock, &p.Category)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, ErrNotFound
    }
    if err != nil {
        return nil, err
    }

    // 3. Simpan ke cache untuk request berikutnya
    s.cache.Set(&p)

    return &p, nil
}

func (s *ProductService) UpdateProduct(ctx context.Context, p *Product) error {
    // Update database
    _, err := s.db.ExecContext(ctx,
        "UPDATE products SET name=?, price=?, stock=? WHERE id=?",
        p.Name, p.Price, p.Stock, p.ID)
    if err != nil {
        return err
    }

    // Invalidate cache
    s.cache.Invalidate(p.ID)
    return nil
}

Contoh Program Lengkap #

package main

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

    "github.com/bradfitz/gomemcache/memcache"
)

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

type Review struct {
    UserID  string
    Rating  int
    Comment string
}

// Cache helper
func setJSON(mc *memcache.Client, key string, value interface{}, expiry int32) error {
    data, _ := json.Marshal(value)
    return mc.Set(&memcache.Item{Key: key, Value: data, Expiration: expiry})
}

func getJSON(mc *memcache.Client, key string, dest interface{}) error {
    item, err := mc.Get(key)
    if err != nil {
        return err
    }
    return json.Unmarshal(item.Value, dest)
}

// Simulasi database
var fakeDB = map[int]Product{
    1: {1, "Laptop Pro 14", 15_000_000, 10, "elektronik"},
    2: {2, "Mouse Wireless", 350_000, 50, "elektronik"},
    3: {3, "Keyboard Mech", 1_500_000, 25, "elektronik"},
}

var dbQueryCount int

func fetchFromDB(id int) (*Product, error) {
    dbQueryCount++
    time.Sleep(50 * time.Millisecond) // simulasi latency DB
    p, ok := fakeDB[id]
    if !ok {
        return nil, errors.New("not found")
    }
    return &p, nil
}

func getProductWithCache(mc *memcache.Client, id int) (*Product, error) {
    key := fmt.Sprintf("product:%d", id)

    var p Product
    if err := getJSON(mc, key, &p); err == nil {
        fmt.Printf("  [HIT]  product:%d\n", id)
        return &p, nil
    }

    fmt.Printf("  [MISS] product:%d — query DB\n", id)
    result, err := fetchFromDB(id)
    if err != nil {
        return nil, err
    }

    setJSON(mc, key, result, 300) // cache 5 menit
    return result, nil
}

func main() {
    mc := memcache.New("localhost:11211")
    mc.Timeout = 100 * time.Millisecond

    if err := mc.Ping(); err != nil {
        log.Fatal("Memcached tidak tersedia:", err)
    }
    fmt.Println("✓ Terhubung ke Memcached\n")

    // Bersihkan cache untuk demo bersih
    mc.DeleteAll()

    // Demo Cache Aside
    fmt.Println("=== Cache Aside Pattern ===")
    fmt.Println("\nRound 1 — semua cache miss:")
    for _, id := range []int{1, 2, 3} {
        p, err := getProductWithCache(mc, id)
        if err != nil {
            log.Println(err)
        } else {
            fmt.Printf("  → %s (Rp%.0f)\n", p.Name, p.Price)
        }
    }

    fmt.Println("\nRound 2 — semua cache hit:")
    for _, id := range []int{1, 2, 3} {
        p, _ := getProductWithCache(mc, id)
        fmt.Printf("  → %s\n", p.Name)
    }

    fmt.Printf("\nTotal DB query: %d (dari 6 request)\n", dbQueryCount)

    // Demo Increment
    fmt.Println("\n=== Atomic Counter ===")
    mc.Set(&memcache.Item{Key: "pageviews:home", Value: []byte("0"), Expiration: 3600})
    for i := 0; i < 5; i++ {
        val, _ := mc.Increment("pageviews:home", 1)
        fmt.Printf("  Pageview #%d\n", val)
    }

    // Demo GetMulti
    fmt.Println("\n=== GetMulti (1 round-trip untuk 3 key) ===")
    keys := []string{"product:1", "product:2", "product:3"}
    items, err := mc.GetMulti(keys)
    if err != nil {
        log.Println(err)
    } else {
        fmt.Printf("  Ditemukan %d dari %d key\n", len(items), len(keys))
        for k, item := range items {
            var p Product
            json.Unmarshal(item.Value, &p)
            fmt.Printf("  %s → %s\n", k, p.Name)
        }
    }

    // Demo TTL — item expire setelah 2 detik
    fmt.Println("\n=== Expiration Demo ===")
    mc.Set(&memcache.Item{Key: "temp:data", Value: []byte("nilai sementara"), Expiration: 2})
    item, _ := mc.Get("temp:data")
    fmt.Printf("  Sebelum expire: %q\n", string(item.Value))

    fmt.Println("  Tunggu 3 detik...")
    time.Sleep(3 * time.Second)

    _, err = mc.Get("temp:data")
    if errors.Is(err, memcache.ErrCacheMiss) {
        fmt.Println("  Setelah expire: cache miss ✓")
    }

    // Demo CAS
    fmt.Println("\n=== CAS (Check-And-Set) ===")
    mc.Set(&memcache.Item{Key: "stock:1", Value: []byte("100"), Expiration: 3600})

    // Simulasi dua goroutine membaca dan mengupdate secara bersamaan
    item1, _ := mc.Gets("stock:1")
    item2, _ := mc.Gets("stock:1")

    // Update pertama berhasil
    item1.Value = []byte("95")
    err = mc.CompareAndSwap(item1)
    fmt.Printf("  Update 1 (kurangi 5): %v\n", err)

    // Update kedua gagal karena data sudah berubah
    item2.Value = []byte("90")
    err = mc.CompareAndSwap(item2)
    fmt.Printf("  Update 2 (kurangi 10): %v (harus ErrCASConflict)\n", err)

    final, _ := mc.Get("stock:1")
    fmt.Printf("  Nilai akhir: %s (seharusnya 95)\n", string(final.Value))
}

Ringkasan #

  • memcache.ErrCacheMiss bukan error kritis — cek dengan errors.Is untuk membedakan miss dari error nyata.
  • GetMulti untuk mengambil banyak key dalam satu round-trip — jauh lebih efisien dari loop Get.
  • Add untuk set hanya jika belum ada (idempotent create); Replace hanya jika sudah ada.
  • Increment/Decrement untuk counter atomik — nilai harus berupa angka dalam format string.
  • CAS (Gets + CompareAndSwap) untuk optimistic lock — retry saat dapat ErrCASConflict.
  • Multi-server dengan consistent hashing otomatis — memcache.New("s1:11211", "s2:11211", "s3:11211").
  • Compression (gzip + gob) untuk menghemat memory saat menyimpan struct besar.
  • Expiration 0 berarti tidak ada expiry — tapi tetap bisa di-evict saat memory penuh (LRU).
  • mc.Timeout wajib dikonfigurasi untuk mencegah goroutine hang saat server Memcached tidak responsif.
  • Memcached tidak punya persistence — jangan simpan data yang tidak bisa diambil ulang dari sumber aslinya.

← Sebelumnya: Redis   Berikutnya: Gin →

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