Irbisa · Cheatsheet
Redis & Caching
Data types, single-threaded execution, persistence, eviction and expiry, pipelining and Lua, cache penetration and stampedes, Sentinel and Cluster
Why is Redis so fast even though it executes commands on a single thread, and what did I/O threads in Redis 6 change?
Which Redis data types do you know, and which backend task does each one fit?
// Errors are ignored for brevity; check them in real code.func dataTypes(ctx context.Context, rdb *redis.Client) error { // String: a cached value with a TTL, or an atomic counter rdb.Set(ctx, "product:42", `{"name":"Desk lamp"}`, 10*time.Minute) rdb.Incr(ctx, "views:product:42")…What is the difference between Redis and Memcached, and when would you still choose Memcached?
How do you put a Redis cache in front of Postgres in a Go service, and how do you prevent a stampede when a hot key expires?
type ProductCache struct { rdb *redis.Client repo *Repo group singleflight.Group}…Find the bug: every cache miss is logged as an error, and the handler answers
500instead of loading the product from Postgres.func (c *Cache) Product(ctx context.Context, id int64) (Product, error) { key := fmt.Sprintf("product:%d", id) b, err := c.rdb.Get(ctx, key).Bytes() if err != nil { c.log.ErrorContext(ctx, "cache get", "key", key, "err", err) return Product{}, err } if b == nil { // miss: load from Postgres and fill the cache return c.load(ctx, key, id) } var p Product err = json.Unmarshal(b, &p) return p, err}Requests for ids that do not exist pass straight through the cache to Postgres. What is cache penetration, and how do negative caching and a Bloom filter stop it?
const notFound = "\x00none" // a marker no real JSON value can equal func (c *Cache) Product(ctx context.Context, id int64) (Product, error) { key := fmt.Sprintf("product:%d", id) b, err := c.rdb.Get(ctx, key).Bytes() if err == nil && string(b) == notFound {…This loop reads 500 prices with one
GETeach and takes about 250 ms, while Redis CPU sits near idle. Why, and what doMGETand pipelining change?func (s *Store) Prices(ctx context.Context, ids []string) (map[string]string, error) { out := make(map[string]string, len(ids)) for _, id := range ids { v, err := s.rdb.Get(ctx, "price:"+id).Result() if errors.Is(err, redis.Nil) { continue // no price cached for this id } if err != nil { return nil, err } out[id] = v } return out, nil}This lookup gives Redis 50 ms, yet when Redis stalls, the handler hangs for seconds. Why does the deadline not apply, and how should the client be configured?
var rdb = redis.NewClient(&redis.Options{Addr: "cache:6379"}) func price(ctx context.Context, sku string) (string, error) { // Redis gets 50 ms; past that the caller reads Postgres instead. ctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) defer cancel() return rdb.Get(ctx, "price:"+sku).Result()}How does Redis delete keys whose TTL has passed, and why can expired keys still take up memory?
func ttlRules(ctx context.Context, rdb *redis.Client) { rdb.Set(ctx, "views", 0, time.Hour) rdb.Incr(ctx, "views") // changes the value, keeps the TTL fmt.Println(rdb.TTL(ctx, "views").Val()) // 1h0m0s rdb.Set(ctx, "views", 5, 0) // replaces the key: the TTL is gone…What happens when Redis reaches
maxmemory, and which eviction policy (allkeys-lru,allkeys-lfu,volatile-ttl,noeviction) fits a cache?How does Redis persist data with RDB snapshots and the AOF, and how many writes can you lose on a crash with each?
# RDB: snapshot after 3600 s if 1 key changed, 300 s if 100, 60 s if 10000save 3600 1 300 100 60 10000 # AOF: log every write, fsync once a second (a crash loses about 1 s)appendonly yesappendfsync everysec…What is atomic in Redis: how do
MULTI/EXEC,WATCHand Lua scripts differ, and why does a Redis transaction never roll back?// WATCH: read, decide in Go, write in MULTI/EXEC; EXEC fails if the key changed meanwhile.func withdraw(ctx context.Context, rdb *redis.Client, key string, amount int64) error { for range 5 { err := rdb.Watch(ctx, func(tx *redis.Tx) error { balance, err := tx.Get(ctx, key).Int64() if err != nil {…Why can a single
KEYS *, or aDELof a two-million-member set, stall Redis for every client, and what doSCANandUNLINKdo instead?// wrong: rdb.Keys(ctx, "session:*") walks every key in one blocking command.// right: SCAN in small steps, and UNLINK so the memory is freed off the main thread.func deleteByPrefix(ctx context.Context, rdb *redis.Client, prefix string) (int, error) { iter := rdb.Scan(ctx, 0, prefix+"*", 500).Iterator() // 500 is a hint, not a limit batch := make([]string, 0, 500) deleted := 0…How do you use Redis as a queue (a list with
BLMOVE, Pub/Sub, or a stream with a consumer group), and what does each lose when a consumer crashes?func consume(ctx context.Context, rdb *redis.Client, consumer string) error { for ctx.Err() == nil { res, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ Group: "billing", Consumer: consumer, Streams: []string{"orders", ">"}, // ">": entries not yet given to anyone in the group…How do Redis Sentinel and Redis Cluster differ, how does Cluster spread keys over 16384 hash slots, and why do some multi-key commands fail with
CROSSSLOT?// Sentinel: one primary; the client asks the sentinels where it is now.func newSentinelClient() *redis.Client { return redis.NewFailoverClient(&redis.FailoverOptions{ MasterName: "orders", SentinelAddrs: []string{"sentinel-1:26379", "sentinel-2:26379", "sentinel-3:26379"}, })…Redis replication is asynchronous. Which acknowledged writes can a failover lose, and what do
WAITandmin-replicas-to-writechange?Redis goes down, every read falls through to Postgres, and Postgres falls over too. What should have been in place before the outage?
var ErrOverloaded = errors.New("overloaded") // the handler answers 503 with Retry-After // DBLoader caps how many cache misses one pod may send to Postgres at once.// Size the cap from what Postgres can serve, divided by the number of pods.type DBLoader struct { slots chan struct{}…