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.ErrCacheMissbukan error kritis — cek denganerrors.Isuntuk membedakan miss dari error nyata.GetMultiuntuk mengambil banyak key dalam satu round-trip — jauh lebih efisien dari loopGet.Adduntuk set hanya jika belum ada (idempotent create);Replacehanya jika sudah ada.Increment/Decrementuntuk counter atomik — nilai harus berupa angka dalam format string.- CAS (
Gets+CompareAndSwap) untuk optimistic lock — retry saat dapatErrCASConflict.- 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
0berarti tidak ada expiry — tapi tetap bisa di-evict saat memory penuh (LRU).mc.Timeoutwajib 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.