Irbisa · Cheatsheet

Redis & Caching

Data types, single-threaded execution, persistence, eviction and expiry, pipelining and Lua, cache penetration and stampedes, Sentinel and Cluster

Middle Developer · 17 questions

  1. 01

    Why is Redis so fast even though it executes commands on a single thread, and what did I/O threads in Redis 6 change?

    Redis keeps all data in memory and runs commands one at a time on one thread over an event loop (epoll on Linux), so a command takes microseconds with no locks.

  2. 02

    Which Redis data types do you know, and which backend task does each one fit?

    Strings hold cached values and counters, hashes hold objects, lists are queues and recent-item feeds, sets keep unique members, sorted sets rank members by a score, and streams are an event log read by consumer groups.

    // 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")…
  3. 03

    What is the difference between Redis and Memcached, and when would you still choose Memcached?

    Memcached is a multi-threaded in-memory cache of plain values, with no persistence or replication.

  4. 04

    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?

    Cache-aside: read Redis; on a miss read Postgres and store the result with a TTL; on a write update Postgres, then delete the key.

    type ProductCache struct {	rdb   *redis.Client	repo  *Repo	group singleflight.Group}…
  5. 05

    Find the bug: every cache miss is logged as an error, and the handler answers 500 instead 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}

    go-redis reports a missing key as the error redis.Nil (its text is redis: nil), not as empty bytes, so every miss takes the err != nil branch and is returned as a failure.

  6. 06

    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?

    Cache penetration is traffic for keys that do not exist: the database finds nothing, so nothing is cached, and every repeat goes to Postgres again.

    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 {…
  7. 07

    This loop reads 500 prices with one GET each and takes about 250 ms, while Redis CPU sits near idle. Why, and what do MGET and 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}

    Each GET is a full network round trip, and the loop waits for every reply before it sends the next request: 500 × about 0.5 ms is 250 ms, while Redis spends microseconds per command.

  8. 08

    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()}

    By default go-redis does not apply the context's deadline to the socket: with ContextTimeoutEnabled false, a read waits up to the client's own ReadTimeout, which is seconds.

  9. 09

    How does Redis delete keys whose TTL has passed, and why can expired keys still take up memory?

    Two ways: lazily, when a command touches a key whose TTL has passed, and actively, in a background cycle that samples keys with a TTL about ten times a second.

    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…
  10. 10

    What happens when Redis reaches maxmemory, and which eviction policy (allkeys-lru, allkeys-lfu, volatile-ttl, noeviction) fits a cache?

    With the default policy, noeviction, writes start failing with OOM command not allowed when used memory > 'maxmemory' while reads still work.

  11. 11

    How does Redis persist data with RDB snapshots and the AOF, and how many writes can you lose on a crash with each?

    RDB writes a point-in-time snapshot from a forked child, so a crash loses everything since the last snapshot, often minutes.

    # 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…
  12. 12

    What is atomic in Redis: how do MULTI/EXEC, WATCH and Lua scripts differ, and why does a Redis transaction never roll back?

    Every single command is atomic, since one thread runs commands one at a time.

    // 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 {…
  13. 13

    Why can a single KEYS *, or a DEL of a two-million-member set, stall Redis for every client, and what do SCAN and UNLINK do instead?

    Redis runs commands on one thread, so an O(N) command blocks every other client until it finishes: KEYS walks the whole keyspace, and DEL of a huge set frees millions of elements on the spot.

    // 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…
  14. 14

    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?

    A list popped with BRPOP loses the message a crashed consumer was holding, Pub/Sub stores nothing and drops messages for anyone not connected, and a stream with a consumer group keeps each entry pending until it is acknowledged, so another consumer can claim a crashed one's work.

    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…
  15. 15

    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 gives one primary automatic failover: sentinels agree that it is down and promote a replica, but all data still lives on one primary.

    // 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"},	})…
  16. 16

    Redis replication is asynchronous. Which acknowledged writes can a failover lose, and what do WAIT and min-replicas-to-write change?

    The primary replies before any replica has the write, so a failover to a lagging replica loses what it never received, and an isolated primary keeps accepting writes that are discarded when it rejoins.

  17. 17

    Redis goes down, every read falls through to Postgres, and Postgres falls over too. What should have been in place before the outage?

    Postgres was sized for the few percent of reads that miss, so losing the cache multiplies its load many times.

    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{}…