Irbisa · Шпаргалка

Redis и кэширование

Типы данных, однопоточное исполнение, персистентность, вытеснение и TTL, pipelining и Lua, cache penetration и stampede, Sentinel и Cluster

Middle-разработчик · 17 вопросов

  1. 01

    Почему Redis такой быстрый, хотя выполняет команды в одном потоке, и что изменили I/O-потоки в Redis 6?

    Redis держит все данные в памяти и выполняет команды по одной в одном потоке поверх цикла событий (epoll в Linux), поэтому команда занимает микросекунды, без блокировок.

  2. 02

    Какие типы данных Redis вы знаете и для какой задачи бэкенда подходит каждый?

    Строки хранят закэшированные значения и счётчики, хеши хранят объекты, списки служат очередями и лентами последних элементов, множества держат уникальные элементы, сортированные множества ранжируют элементы по score, а стримы представляют собой лог событий, который читают группы консьюмеров.

    // 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

    Чем Redis отличается от Memcached и когда всё же стоит выбрать Memcached?

    Memcached представляет собой многопоточный кэш простых значений в памяти, без персистентности и репликации.

  4. 04

    Как поставить кэш Redis перед Postgres в Go-сервисе и как не допустить cache stampede (лавину промахов), когда истекает горячий ключ?

    Cache-aside: читаем Redis; при промахе читаем Postgres и кладём результат с TTL; при записи обновляем Postgres и удаляем ключ.

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

    Найдите баг: каждый промах кэша пишется в лог как ошибка, и хендлер отвечает 500 вместо того, чтобы загрузить товар из 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 сообщает об отсутствующем ключе ошибкой redis.Nil (её текст redis: nil), а не пустыми байтами, поэтому каждый промах уходит в ветку err != nil и возвращается как сбой.

  6. 06

    Запросы по несуществующим id проходят сквозь кэш прямо в Postgres. Что такое cache penetration и как его останавливают негативное кэширование и фильтр Блума?

    Cache penetration означает трафик по ключам, которых не существует: база ничего не находит, в кэш ничего не попадает, и каждый повтор снова идёт в Postgres.

    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

    Этот цикл читает 500 цен, по одному GET на каждую, и занимает около 250 мс, а CPU у Redis почти простаивает. Почему, и что меняют MGET и пайплайнинг?

    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}

    Каждый GET означает полный сетевой round trip, и цикл ждёт каждого ответа, прежде чем отправить следующий запрос: 500 × примерно 0,5 мс дают 250 мс, тогда как сам Redis тратит на команду микросекунды.

  8. 08

    Этот запрос даёт Redis 50 мс, но когда Redis подвисает, хендлер висит секундами. Почему дедлайн не срабатывает и как настроить клиент?

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

    По умолчанию go-redis не переносит дедлайн контекста на сокет: при ContextTimeoutEnabled, равном false, чтение ждёт до собственного ReadTimeout клиента, а это секунды.

  9. 09

    Как Redis удаляет ключи с истёкшим TTL и почему истёкшие ключи всё ещё могут занимать память?

    Двумя способами: лениво, когда команда обращается к ключу с истёкшим TTL, и активно, в фоновом цикле, который примерно десять раз в секунду проверяет выборку ключей с TTL.

    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

    Что происходит, когда Redis достигает maxmemory, и какая политика вытеснения (allkeys-lru, allkeys-lfu, volatile-ttl, noeviction) подходит для кэша?

    С политикой по умолчанию, noeviction, запись начинает падать с ошибкой OOM command not allowed when used memory > 'maxmemory', а чтение продолжает работать.

  11. 11

    Как Redis сохраняет данные через RDB-снапшоты и AOF и сколько записей можно потерять при падении в каждом случае?

    RDB записывает снапшот на момент времени из дочернего процесса после fork, поэтому при падении теряется всё, что изменилось после последнего снапшота, часто за минуты.

    # 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

    Что в Redis атомарно: чем отличаются MULTI/EXEC, WATCH и Lua-скрипты и почему транзакция Redis никогда не откатывается?

    Атомарна каждая отдельная команда, потому что один поток выполняет команды по одной.

    // 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

    Почему один KEYS * или DEL множества из двух миллионов элементов может затормозить Redis для всех клиентов и что вместо этого делают SCAN и UNLINK?

    Redis выполняет команды в одном потоке, поэтому команда с работой O(N) блокирует всех остальных клиентов, пока не закончит: KEYS обходит всё пространство ключей, а DEL огромного множества тут же освобождает миллионы элементов.

    // 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

    Как использовать Redis как очередь (список с BLMOVE, Pub/Sub или стрим с группой консьюмеров) и что теряет каждый вариант, когда консьюмер падает?

    Список, из которого читают через BRPOP, теряет сообщение, которое держал упавший консьюмер; Pub/Sub ничего не хранит и теряет сообщения для всех, кто не подключён; а стрим с группой консьюмеров держит каждую запись в ожидающих, пока её не подтвердят, поэтому работу упавшего консьюмера может забрать другой.

    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

    Чем отличаются Redis Sentinel и Redis Cluster, как Cluster распределяет ключи по 16384 hash slot и почему некоторые команды с несколькими ключами падают с CROSSSLOT?

    Sentinel даёт одному мастеру автоматический failover: сентинелы сходятся во мнении, что он недоступен, и повышают реплику, но все данные по-прежнему живут на одном мастере.

    // 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 асинхронная. Какие подтверждённые записи может потерять failover и что меняют WAIT и min-replicas-to-write?

    Мастер отвечает раньше, чем запись получит хоть одна реплика, поэтому failover на отстающую реплику теряет то, что до неё не дошло, а изолированный мастер продолжает принимать записи, которые выбрасываются при его возвращении.

  17. 17

    Redis падает, все чтения проваливаются в Postgres, и Postgres тоже ложится. Что должно было быть сделано до аварии?

    Postgres рассчитывали на несколько процентов чтений, которые промахиваются мимо кэша, поэтому потеря кэша умножает его нагрузку во много раз.

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