Redis в Go: кеши, локи, очереди
Зачем знать: Redis — швейцарский нож backend-инфраструктуры: кеш, session store, rate limiter, distributed lock, pub/sub, lightweight queue (Streams). Middle 1 Go-разработчик в 2026 должен уверенно пользоваться
redis/go-redis/v9, понимать pipelining, transactions, cache stampede и почему Redlock не безопасен в общем случае.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- Как в Go (с примерами)
- Gotchas
- Best practices в production
- Вопросы на собесе
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»Что такое Redis
Заголовок раздела «Что такое Redis»Redis (REmote DIctionary Server) — in-memory key-value store с persistence. Однопоточный (для commands), но event loop позволяет выдерживать 100K+ ops/sec на одной машине. В 2026 актуальная версия — Redis 7.4+ (LTS 7.2).
Use cases
Заголовок раздела «Use cases»- Cache — самое частое, ускорение БД-запросов.
- Session store — JWT не идеален для logout, Redis session с TTL.
- Rate limiter — atomic INCR + EXPIRE.
- Distributed lock — SET NX EX.
- Counter / leaderboard — Sorted sets (ZADD, ZRANGE).
- Pub/Sub — fire-and-forget сообщения.
- Streams — persistent журнал событий (5.0+), альтернатива Kafka для среднего масштаба.
- Geo-search — GEOADD, GEOSEARCH.
- Bitmap, HyperLogLog — для analytics.
Структуры данных
Заголовок раздела «Структуры данных»| Тип | Команды | Use case |
|---|---|---|
| String | GET, SET, INCR, APPEND | счётчики, кеш |
| Hash | HSET, HGET, HGETALL | объекты (user:123) |
| List | LPUSH, RPUSH, LPOP, BLPOP | очереди, истории |
| Set | SADD, SISMEMBER, SUNION | уникальные, теги |
| Sorted Set | ZADD, ZRANGE, ZRANGEBYSCORE | leaderboards, time-series |
| Stream | XADD, XREAD, XREADGROUP | append-only лог |
| Bitmap | SETBIT, GETBIT, BITCOUNT | feature flags по user_id |
| HyperLogLog | PFADD, PFCOUNT | unique counts, ~1% error |
| Geo | GEOADD, GEOSEARCH | гео-поиск |
| JSON (модуль) | JSON.SET, JSON.GET | structured docs |
Persistence
Заголовок раздела «Persistence»- RDB — snapshot (fork + dump). Дефолт.
- AOF — append-only file (каждая команда). Безопаснее, но больше IO.
- Hybrid — RDB snapshot + AOF с last snapshot.
В большинстве cache-сценариев persistence отключают (кеш можно перепрогреть).
Топологии
Заголовок раздела «Топологии»- Single instance — для dev, small prod.
- Replication (master + N replicas) — масштабирование чтений, HA через Redis Sentinel.
- Cluster — sharding (16384 slots), data partitioning, HA встроенно.
- Redis Enterprise / KeyDB / Dragonfly — коммерческие/альтернативные.
Memory eviction
Заголовок раздела «Memory eviction»Когда maxmemory достигнут, Redis удаляет согласно maxmemory-policy:
noeviction— ошибка на write.allkeys-lru— выкидывает least recently used.allkeys-lfu— least frequently used.volatile-lru/lfu— только ключи с TTL.volatile-ttl— ключи с близким expire.volatile-random / allkeys-random.
Для кеша обычно allkeys-lfu (better hit rate).
2. Как в Go (с примерами)
Заголовок раздела «2. Как в Go (с примерами)»2.1 Установка
Заголовок раздела «2.1 Установка»go get github.com/redis/go-redis/v9В 2026 актуальна v9. Старый go-redis/redis/v8 deprecated (RESP2 only).
2.2 Подключение
Заголовок раздела «2.2 Подключение»package main
import ( "context" "github.com/redis/go-redis/v9")
func main() { rdb := redis.NewClient(&redis.Options{ Addr: "redis:6379", Password: "", DB: 0, PoolSize: 50, // default = 10 * GOMAXPROCS MinIdleConns: 10, ReadTimeout: 500 * time.Millisecond, WriteTimeout: 500 * time.Millisecond, DialTimeout: 2 * time.Second, })
ctx := context.Background() if err := rdb.Ping(ctx).Err(); err != nil { panic(err) }}2.3 Базовые команды
Заголовок раздела «2.3 Базовые команды»// SET / GETerr := rdb.Set(ctx, "user:1", "alice", 10*time.Minute).Err()val, err := rdb.Get(ctx, "user:1").Result() // "alice"
if err == redis.Nil { // ключа нет}
// INCR (atomic)n, _ := rdb.Incr(ctx, "counter").Result()
// TTLttl, _ := rdb.TTL(ctx, "user:1").Result() // 9m59s
// DELdeleted, _ := rdb.Del(ctx, "user:1").Result() // 1
// EXPIRErdb.Expire(ctx, "user:1", 5*time.Minute)2.4 Hash
Заголовок раздела «2.4 Hash»rdb.HSet(ctx, "user:1", map[string]any{ "name": "alice", "age": 30,})
age, _ := rdb.HGet(ctx, "user:1", "age").Int()all, _ := rdb.HGetAll(ctx, "user:1").Result() // map[string]string
rdb.HIncrBy(ctx, "user:1", "balance", 100)2.5 List как очередь
Заголовок раздела «2.5 List как очередь»// Producerrdb.RPush(ctx, "queue", "task1", "task2")
// Consumer (блокирующий)res, err := rdb.BLPop(ctx, 5*time.Second, "queue").Result()// res[0] = "queue", res[1] = "task1"2.6 Set
Заголовок раздела «2.6 Set»rdb.SAdd(ctx, "tags:post:1", "go", "redis", "cache")isMember, _ := rdb.SIsMember(ctx, "tags:post:1", "go").Result()all, _ := rdb.SMembers(ctx, "tags:post:1").Result()2.7 Sorted Set (leaderboard)
Заголовок раздела «2.7 Sorted Set (leaderboard)»rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 100, Member: "alice"}, redis.Z{Score: 95, Member: "bob"},)
// Top 10top, _ := rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, 9).Result()2.8 Pipelining
Заголовок раздела «2.8 Pipelining»pipe := rdb.Pipeline()incr := pipe.Incr(ctx, "counter")expire := pipe.Expire(ctx, "counter", time.Hour)_, err := pipe.Exec(ctx)
if err == nil { val, _ := incr.Result() _ = val _ = expire.Val()}Pipelining = одна сетевая поездка для N команд. Не атомарность — другие клиенты могут вмешаться между командами.
2.9 Transactions
Заголовок раздела «2.9 Transactions»err := rdb.Watch(ctx, func(tx *redis.Tx) error { n, err := tx.Get(ctx, "counter").Int() if err != nil && err != redis.Nil { return err } n++
_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { pipe.Set(ctx, "counter", n, 0) return nil }) return err}, "counter") // watched keysWATCH + MULTI + EXEC = optimistic locking. Если другой клиент изменил counter между WATCH и EXEC — txf возвращает redis.TxFailedErr (retry).
2.10 Pub/Sub
Заголовок раздела «2.10 Pub/Sub»// Subscribersub := rdb.Subscribe(ctx, "events")defer sub.Close()
ch := sub.Channel()for msg := range ch { fmt.Println(msg.Channel, msg.Payload)}
// Publisherrdb.Publish(ctx, "events", "hello")Pub/Sub fire-and-forget — если подписчика нет, сообщение теряется. Для надёжной доставки — Streams.
2.11 Streams (как Kafka lite)
Заголовок раздела «2.11 Streams (как Kafka lite)»// Producerid, _ := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "events", MaxLen: 10000, // обрезка Values: map[string]any{"user_id": "u-123", "action": "buy"},}).Result()
// Consumer Grouprdb.XGroupCreateMkStream(ctx, "events", "workers", "$")
// Consumerfor { res, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ Group: "workers", Consumer: "worker-1", Streams: []string{"events", ">"}, // ">" = только новые Count: 10, Block: 5 * time.Second, }).Result() if err != nil { continue } for _, stream := range res { for _, msg := range stream.Messages { process(msg.Values) rdb.XAck(ctx, "events", "workers", msg.ID) } }}Если consumer крашится не делая XACK, при XPENDING/XCLAIM сообщение можно забрать другим consumer.
2.12 Cache-aside pattern
Заголовок раздела «2.12 Cache-aside pattern»type UserCache struct { rdb *redis.Client db *Repository ttl time.Duration}
func (c *UserCache) Get(ctx context.Context, id string) (*User, error) { key := "user:" + id
// Try cache data, err := c.rdb.Get(ctx, key).Bytes() if err == nil { var u User if err := json.Unmarshal(data, &u); err == nil { return &u, nil } }
// Cache miss → DB u, err := c.db.GetUser(ctx, id) if err != nil { return nil, err }
// Write back to cache (best-effort) if b, err := json.Marshal(u); err == nil { c.rdb.Set(ctx, key, b, c.ttl) } return u, nil}
func (c *UserCache) Invalidate(ctx context.Context, id string) error { return c.rdb.Del(ctx, "user:"+id).Err()}2.13 Cache stampede (thundering herd)
Заголовок раздела «2.13 Cache stampede (thundering herd)»Если популярный ключ истёк, все запросы одновременно ходят в БД. Решение — singleflight:
import "golang.org/x/sync/singleflight"
var sfg singleflight.Group
func (c *UserCache) Get(ctx context.Context, id string) (*User, error) { if u, err := c.getFromCache(ctx, id); err == nil { return u, nil }
res, err, _ := sfg.Do(id, func() (interface{}, error) { u, err := c.db.GetUser(ctx, id) if err != nil { return nil, err } c.setCache(ctx, id, u) return u, nil }) if err != nil { return nil, err } return res.(*User), nil}Все одновременные запросы за одним ID сольются в один поход в БД.
2.14 Distributed lock (basic)
Заголовок раздела «2.14 Distributed lock (basic)»func acquireLock(ctx context.Context, rdb *redis.Client, key, val string, ttl time.Duration) (bool, error) { return rdb.SetNX(ctx, "lock:"+key, val, ttl).Result()}
func releaseLock(ctx context.Context, rdb *redis.Client, key, val string) error { // Lua-script для атомарной проверки + удаления script := redis.NewScript(` if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end `) _, err := script.Run(ctx, rdb, []string{"lock:" + key}, val).Result() return err}val обычно UUID — чтобы не удалить чужой lock после своего timeout.
2.15 Redsync (распределённый lock)
Заголовок раздела «2.15 Redsync (распределённый lock)»import "github.com/go-redsync/redsync/v4"
pool := goredis.NewPool(rdb)rs := redsync.New(pool)
mutex := rs.NewMutex("my-resource", redsync.WithExpiry(10*time.Second))if err := mutex.LockContext(ctx); err != nil { return err}defer mutex.UnlockContext(ctx)Внимание: Redlock алгоритм (Antirez) критикован Martin Kleppmann — в edge cases (clock skew, network partitions) даёт два владельца одновременно. Не использовать для критических transactions (деньги, инвентарь).
2.16 Rate limiter (token bucket)
Заголовок раздела «2.16 Rate limiter (token bucket)»-- rate_limit.lualocal key = KEYS[1]local limit = tonumber(ARGV[1])local window = tonumber(ARGV[2])local current = redis.call("INCR", key)if current == 1 then redis.call("EXPIRE", key, window)endif current > limit then return 0endreturn 1script := redis.NewScript(rateLimitLua)
func allow(ctx context.Context, rdb *redis.Client, userID string) bool { key := "rate:" + userID res, err := script.Run(ctx, rdb, []string{key}, 100, 60).Int() return err == nil && res == 1}100 запросов за 60 секунд. Lua атомарен (Redis однопоточный для commands).
2.17 Sentinel
Заголовок раздела «2.17 Sentinel»rdb := redis.NewFailoverClient(&redis.FailoverOptions{ MasterName: "mymaster", SentinelAddrs: []string{"sentinel-1:26379", "sentinel-2:26379"},})Sentinel мониторит master, при отказе делает failover.
2.18 Cluster
Заголовок раздела «2.18 Cluster»rdb := redis.NewClusterClient(&redis.ClusterOptions{ Addrs: []string{"node-1:6379", "node-2:6379", "node-3:6379"},})Cluster: 16384 slots распределены между nodes. Ключи {tag}suffix попадают в один slot (для multi-key операций).
2.19 Tracing/metrics
Заголовок раздела «2.19 Tracing/metrics»import "github.com/redis/go-redis/extra/redisotel/v9"import "github.com/redis/go-redis/extra/redisprometheus/v9"
redisotel.InstrumentTracing(rdb)redisotel.InstrumentMetrics(rdb)prometheus.MustRegister(redisprometheus.NewCollector("myapp", "redis", rdb))2.20 Health check
Заголовок раздела «2.20 Health check»if err := rdb.Ping(ctx).Err(); err != nil { // readiness fail}3. Gotchas
Заголовок раздела «3. Gotchas»3.1 redis.Nil не error
Заголовок раздела «3.1 redis.Nil не error»val, err := rdb.Get(ctx, "k").Result()if err == redis.Nil { // нет ключа — это нормально} else if err != nil { // настоящая ошибка}Многие используют errors.Is(err, redis.Nil) — корректно.
3.2 KEYS — никогда в prod
Заголовок раздела «3.2 KEYS — никогда в prod»keys, _ := rdb.Keys(ctx, "user:*").Result() // BADKEYS блокирует Redis на время сканирования (O(N) от размера базы). На 10М ключей = 5 секунд freeze. Используйте SCAN:
iter := rdb.Scan(ctx, 0, "user:*", 100).Iterator()for iter.Next(ctx) { key := iter.Val() // ...}3.3 FLUSHDB / FLUSHALL
Заголовок раздела «3.3 FLUSHDB / FLUSHALL»Не запускать на prod. Если очень нужно — --lazy (async).
3.4 Pub/Sub теряет сообщения
Заголовок раздела «3.4 Pub/Sub теряет сообщения»Если подписчик offline → сообщение потеряно. Pub/Sub не для критичных событий. Используйте Streams или Kafka.
3.5 Большие значения
Заголовок раздела «3.5 Большие значения»Значение > 100KB замедляет всё (сетевая bandwidth, парсинг). Не храните blob в Redis — S3.
3.6 Большие коллекции
Заголовок раздела «3.6 Большие коллекции»HGETALL на hash с 100K полей = 100K строк в одном reply = задержка всем клиентам. Используйте HSCAN.
3.7 EXPIRE — race с обновлением
Заголовок раздела «3.7 EXPIRE — race с обновлением»rdb.Set(ctx, "k", v, 0) // без TTLrdb.Expire(ctx, "k", time.Minute) // отдельной командойМежду этими командами кто-то может SET без TTL — потеряете expire. Лучше Set с TTL атомарно.
3.8 Pipelining не атомарен
Заголовок раздела «3.8 Pipelining не атомарен»Между командами в pipeline другие клиенты могут вмешаться. Для атомарности — TxPipelined (MULTI/EXEC) или Lua-скрипт.
3.9 Cluster: multi-key команды
Заголовок раздела «3.9 Cluster: multi-key команды»rdb.MGet(ctx, "user:1", "user:2") // в Cluster может упасть CROSSSLOTВ Cluster MGet/MSET работают только если все ключи в одном слоте. Используйте {tag} для группировки: {user}1, {user}2.
3.10 Connection pool
Заголовок раздела «3.10 Connection pool»Default 10 × NumCPU. На 32-core — 320 соединений (Redis по умолчанию maxclients=10000). Под нагрузкой maxclients исчерпывается → ошибки.
Настройте PoolSize явно. Метрика redis_pool_wait_count через redisprometheus.
3.11 TTL для всех ключей
Заголовок раздела «3.11 TTL для всех ключей»Без TTL ключ остаётся вечно → memory растёт. Каждый кеш-ключ должен иметь TTL. MAXMEMORY + allkeys-lfu спасают, но лучше эксплицитно.
3.12 Cache invalidation
Заголовок раздела «3.12 Cache invalidation»«Two hard problems in computer science: cache invalidation, naming things, and off-by-one errors». Стратегии:
- TTL (просто, eventually consistent).
- Delete on write.
- Write-through (записываем в кеш одновременно с БД).
- Event-driven (CDC/Outbox → invalidate).
3.13 Race condition в read-modify-write
Заголовок раздела «3.13 Race condition в read-modify-write»val, _ := rdb.Get(ctx, "balance").Int()val += 100rdb.Set(ctx, "balance", val, 0) // BAD: raceИспользуйте INCRBY или WATCH/MULTI.
3.14 Hot key
Заголовок раздела «3.14 Hot key»Один популярный ключ (featured_product) — все запросы туда → CPU bottleneck. Решения: реплики (для чтения), local cache над Redis, шардирование по бакетам.
3.15 Lua-скрипты — atomic, но блокируют
Заголовок раздела «3.15 Lua-скрипты — atomic, но блокируют»Долгий Lua-скрипт блокирует Redis для других клиентов (одного потока).
3.16 String vs Int
Заголовок раздела «3.16 String vs Int»rdb.Get(ctx, "k").Int() парсит string из Redis в int. Если значение содержит non-numeric — ошибка.
3.17 Persistence на cache
Заголовок раздела «3.17 Persistence на cache»В 2026 для cache часто отключают AOF и даже RDB. Снижает IO. Restart = прогрев заново.
3.18 Память: фрагментация
Заголовок раздела «3.18 Память: фрагментация»mem_fragmentation_ratio > 1.5 — память фрагментирована. Команда MEMORY PURGE помогает; в новых версиях есть activedefrag.
4. Best practices в production
Заголовок раздела «4. Best practices в production»4.1 Соединения с лимитами
Заголовок раздела «4.1 Соединения с лимитами»rdb := redis.NewClient(&redis.Options{ Addr: "...", PoolSize: 50, MinIdleConns: 10, PoolTimeout: 2 * time.Second, ConnMaxIdleTime: 5 * time.Minute, ConnMaxLifetime: time.Hour, DialTimeout: 2 * time.Second, ReadTimeout: 500 * time.Millisecond, WriteTimeout: 500 * time.Millisecond,})4.2 Контекст с deadline
Заголовок раздела «4.2 Контекст с deadline»ctx, cancel := context.WithTimeout(parent, 200*time.Millisecond)defer cancel()rdb.Get(ctx, "k")Без deadline — поход в Redis может зависнуть, request у пользователя — тоже.
4.3 Ключи: namespacing
Заголовок раздела «4.3 Ключи: namespacing»service:entity:id:fieldpayments:user:123:balancesessions:active:s-abccache:product:42rate:user:123lock:order:42Convention: : как разделитель, нижний регистр.
4.4 TTL обязательно
Заголовок раздела «4.4 TTL обязательно»rdb.Set(ctx, "cache:...", v, 10*time.Minute)Никаких persistent ключей в Redis-кеше.
4.5 maxmemory + eviction
Заголовок раздела «4.5 maxmemory + eviction»maxmemory 4gbmaxmemory-policy allkeys-lfuВ Redis Cluster — на каждой ноде.
4.6 Структурированные сериализаторы
Заголовок раздела «4.6 Структурированные сериализаторы»JSON удобен, но дорог. Для hot path — Protobuf, MessagePack, или Hash (без сериализации, native структура).
4.7 Singleflight для дорогих cache-miss
Заголовок раздела «4.7 Singleflight для дорогих cache-miss»См. 2.13. Защита от thundering herd.
4.8 Two-level cache
Заголовок раздела «4.8 Two-level cache»Local in-memory (ristretto, freecache) + Redis. Local read = nanoseconds, Redis = ~1ms, БД = ~10ms.
v, found := localCache.Get(k)if !found { v, _ = rdb.Get(ctx, k).Bytes() localCache.SetWithTTL(k, v, 1, 30*time.Second)}4.9 Sentinel/Cluster для prod
Заголовок раздела «4.9 Sentinel/Cluster для prod»Single instance — только staging/dev. Prod — Sentinel (replicas) или Cluster (sharding).
4.10 Backup
Заголовок раздела «4.10 Backup»RDB snapshot периодически выгружать в S3. Без этого при катастрофе данные пропадут.
4.11 Мониторинг
Заголовок раздела «4.11 Мониторинг»Метрики Redis (через exporter): hit rate, memory, evictions, connected_clients, blocked_clients.
Метрики клиента: pool_hits, pool_misses, pool_timeouts.
4.12 Slow log
Заголовок раздела «4.12 Slow log»CONFIG SET slowlog-log-slower-than 10000 (10ms). SLOWLOG GET 10 показывает медленные команды.
4.13 Не использовать KEYS в prod
Заголовок раздела «4.13 Не использовать KEYS в prod»См. 3.2. SCAN или явные индексы (Set).
4.14 TLS для прода
Заголовок раздела «4.14 TLS для прода»Redis 6+ поддерживает TLS. В public cloud — обязательно.
4.15 ACL и пароли
Заголовок раздела «4.15 ACL и пароли»Redis 6+: ACL SETUSER. Один пароль для всех — антипаттерн. Отдельный user на сервис с минимальными command permissions.
4.16 Не делать ACID в Redis
Заголовок раздела «4.16 Не делать ACID в Redis»Redis даёт atomicity в рамках команды/Lua/MULTI, но не durability (persistence по умолчанию async). Для денежных транзакций — БД с ACID.
4.17 Streams вместо Pub/Sub для надёжности
Заголовок раздела «4.17 Streams вместо Pub/Sub для надёжности»Если потеря сообщения недопустима — Streams + Consumer Groups + ACK + XPENDING для retry.
4.18 OpenTelemetry tracing
Заголовок раздела «4.18 OpenTelemetry tracing»См. 2.19. В trace каждая Redis-команда — отдельный span.
4.19 Health probe — простой
Заголовок раздела «4.19 Health probe — простой»/ready не должен делать тяжёлый Redis-call. Простой PING с timeout.
4.20 Idempotency keys
Заголовок раздела «4.20 Idempotency keys»Для retry-safe операций: храните idempotency_key → response с TTL. При повторе клиент получает тот же результат.
5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»-
Чем Redis отличается от memcached? Redis: больше типов данных, persistence, replication, transactions, scripts, streams. Memcached проще, чисто LRU cache.
-
Какие use cases у Redis? Cache, session, rate limit, lock, leaderboard, queue (List/Stream), pub/sub, counter.
-
Почему Redis быстрый? In-memory, однопоточная обработка commands (нет lock contention), эффективный event loop, оптимизированные структуры данных.
-
Что такое pipelining? Отправка нескольких команд одним сетевым раундтрипом. Снижает latency. Не атомарно.
-
Чем MULTI/EXEC отличается от Pipeline? MULTI/EXEC — атомарность (все команды или ни одной). Pipeline — просто batching, между командами могут вклиниться другие.
-
Зачем WATCH? Optimistic locking. WATCH ключ → MULTI/EXEC выполнится только если ключ не изменился.
-
Что такое Pub/Sub и его ограничения? Fire-and-forget публикация. Подписчик offline → сообщение потеряно. Для надёжности — Streams.
-
Чем Streams лучше Pub/Sub? Persistent (хранит сообщения), Consumer Groups (как Kafka), ACK, replay, XCLAIM для retry.
-
Что такое cache-aside pattern? Приложение: cache miss → fetch from DB → populate cache. Дефолт.
-
Что такое cache stampede и как защититься? Многие запросы одновременно идут в БД при истечении популярного ключа. Решение — singleflight.
-
Чем INCR отличается от GET + SET + 1? INCR атомарен на стороне Redis. GET+SET — race (два клиента одновременно прочитали и записали).
-
Что такое распределённый lock? Эксклюзивный доступ к ресурсу из разных процессов. В Redis — SET NX EX.
-
Безопасен ли Redlock? В edge cases (clock skew, GC pause, network partition) — нет (Martin Kleppmann). Для критичных систем — Zookeeper/etcd или DB row lock.
-
Какие eviction policies? noeviction, allkeys-lru, allkeys-lfu, volatile-lru, volatile-lfu, volatile-ttl, allkeys-random, volatile-random.
-
Чем Sentinel отличается от Cluster? Sentinel — HA (master + replicas + failover), не sharding. Cluster — sharding (16384 slots) + HA.
-
Что такое hot key и как решить? Один ключ получает 90% запросов → CPU bottleneck. Решения: реплики чтения, local cache, шардирование по бакетам.
-
Почему нельзя KEYS в prod? O(N) от всего keyspace, блокирует Redis на длительное время. SCAN — incremental, non-blocking.
-
Что такое connection pool? Set переиспользуемых TCP-соединений к Redis. PoolSize ограничивает concurrent.
-
Persistence: RDB vs AOF? RDB — snapshot (компактнее, быстрее восстановление). AOF — append-only log (надёжнее, fsync per second/always).
-
Когда не использовать Redis? Когда нужна сильная durability (деньги), сложные запросы (joins), большие blob (>100KB).
-
Что такое Lua-скрипт в Redis? Скрипт, выполняемый атомарно (Redis однопоточный). Полезно для rate limit, atomic check-and-set.
-
Чем
HSETотличается отSET? SET — string-значение. HSET — hash (объект с полями). HSET не имеет TTL на поле, только на весь hash. -
Что такое CROSSSLOT в Cluster? Ошибка при multi-key операции на ключах из разных slots. Решение:
{tag}для группировки. -
Как сделать idempotency через Redis? Хранить
idempotency_key → responseс TTL. При повторе — вернуть кешированный ответ. -
Что такое HyperLogLog? Approximate cardinality (count unique). Использует ~12KB на бесконечное множество с ~1% ошибкой. PFADD, PFCOUNT.
6. Practice
Заголовок раздела «6. Practice»Упражнение 1: Кеш с TTL
Заголовок раздела «Упражнение 1: Кеш с TTL»Реализуйте UserCache с методами Get(id), Set(user), Invalidate(id). TTL 5 минут.
Упражнение 2: Singleflight
Заголовок раздела «Упражнение 2: Singleflight»Добавьте singleflight в Get, чтобы при одновременных miss на один и тот же ID был один поход в БД.
Упражнение 3: Distributed lock
Заголовок раздела «Упражнение 3: Distributed lock»Реализуйте Lock(ctx, resource, ttl) через SET NX EX + UUID + Lua release. Напишите тест с двумя горутинами.
Упражнение 4: Rate limiter
Заголовок раздела «Упражнение 4: Rate limiter»Lua-скрипт: 100 запросов на user_id за 60 секунд. Используйте INCR + EXPIRE атомарно.
Упражнение 5: Sorted Set leaderboard
Заголовок раздела «Упражнение 5: Sorted Set leaderboard»Топ-10 пользователей по очкам. ZADD на каждом действии, ZREVRANGEWITHSCORES для топа.
Упражнение 6: Streams очередь
Заголовок раздела «Упражнение 6: Streams очередь»Producer добавляет события в stream orders. Consumer Group workers обрабатывает с XACK. При перезапуске consumer получает unacked сообщения через XCLAIM.
Упражнение 7: Pipelining
Заголовок раздела «Упражнение 7: Pipelining»Сравните latency 100 INCR через for-loop и через Pipeline. Замерьте через time.Since.
Упражнение 8: Pub/Sub
Заголовок раздела «Упражнение 8: Pub/Sub»Subscriber на канал notifications, publisher шлёт каждые 5 секунд. Покажите, что при отключении subscriber сообщения теряются.
Упражнение 9: Cluster CROSSSLOT
Заголовок раздела «Упражнение 9: Cluster CROSSSLOT»В Redis Cluster попробуйте MGET двух ключей без {tag} — увидите ошибку. Добавьте {user}1, {user}2 — заработает.
Упражнение 10: Trace+Metrics
Заголовок раздела «Упражнение 10: Trace+Metrics»Подключите redisotel и redisprometheus. Откройте Grafana — увидьте метрики пула и spans в трассах.
7. Источники
Заголовок раздела «7. Источники»- Официальная документация
go-redis/v9— https://redis.uptrace.dev/. - Redis docs — https://redis.io/docs/.
- Redis Best Practices (AWS) — https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/BestPractices.html.
- Martin Kleppmann: “How to do distributed locking” — https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html.
- Antirez (Salvatore Sanfilippo) blog — http://antirez.com/ (создатель Redis).
- Redis Streams introduction — https://redis.io/docs/data-types/streams/.
- redsync — https://github.com/go-redsync/redsync.
- “Redis in Action” by Josiah Carlson (Manning, 2013) — устарела, но фундаменты живы.
- “Database Internals” by Alex Petrov (O’Reilly, 2019) — для понимания TS DB.
- Redis Cluster spec — https://redis.io/docs/reference/cluster-spec/.