Локальный кэш для снижения латентности при работе с удалённым key‑value хранилищем

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

Короткий ответ

Что ответить на собеседовании

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

Подробный разбор

Ответ с пояснениями

Условие

В системе используется интерфейс для доступа к key‑value базе, размещённой в США, тогда как клиентская часть работает в Европе. Большое географическое расстояние приводит к заметным задержкам при каждом запросе к базе. Чтобы ускорить отклик, требуется внедрить слой кэширования. Для этого следует применить паттерн «Декоратор», который обернёт оригинальную реализацию интерфейса и будет скрывать внутри себя локальный кэш.

type KVDatabase interface {
  Get(string) (string, error)
  GetKeys() ([]string, error)
}

type RedisDatabase struct {
  // ...
}

func (d *RedisDatabase) Get(key string) (string, error) {
  // ...
}

func (d *RedisDatabase) GetKeys() ([]string, error) {
  // ...
}

Ответ

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

Упрощённый вариант с sync, time и golang.org/x/sync/singleflight:

type cachedValue struct { value string; expires time.Time }
type CachedDB struct {
    inner KVDatabase
    ttl time.Duration
    max int
    mu sync.Mutex
    values map[string]cachedValue
    flights singleflight.Group
}
func NewCachedDB(inner KVDatabase, ttl time.Duration, max int) *CachedDB {
    if ttl <= 0 || max <= 0 { panic("invalid cache limits") }
    return &CachedDB{inner: inner, ttl: ttl, max: max,
        values: make(map[string]cachedValue)}
}
func (d *CachedDB) lookup(key string) (string, bool) {
    d.mu.Lock()
    defer d.mu.Unlock()
    item, ok := d.values[key]
    if ok && time.Now().Before(item.expires) { return item.value, true }
    delete(d.values, key)
    return "", false
}
func (d *CachedDB) Get(key string) (string, error) {
    if value, ok := d.lookup(key); ok { return value, nil }
    result, err, _ := d.flights.Do(key, func() (any, error) {
        if value, ok := d.lookup(key); ok { return value, nil }
        value, err := d.inner.Get(key)
        if err != nil { return "", err }
        d.mu.Lock()
        if len(d.values) >= d.max {
            for old := range d.values { delete(d.values, old); break }
        }
        d.values[key] = cachedValue{value, time.Now().Add(d.ttl)}
        d.mu.Unlock()
        return value, nil
    })
    if err != nil { return "", err }
    return result.(string), nil
}
func (d *CachedDB) GetKeys() ([]string, error) { return d.inner.GetKeys() }

При заполнении удаляется произвольная запись; это ограниченный кэш, не LRU. GetKeys намеренно делегирован источнику: локальная map содержит лишь посещённые ключи и не может заменить полный каталог. TTL задаёт допустимую устарелость, ошибки не кэшируются. Для изменяемых данных нужны согласованные правила инвалидирования; удалённый клиент должен ограничивать время запроса.

Источник: pkg.go.dev.

ИИ-помощник для собеседований

Хочешь уверенно проходить собеседования?

Попробуй ИИ-помощник для собеседований: слышит вас и собеседника, анализирует экран, подсказывает ответы в реальном времени, работает без VPN и не попадает в захват экрана.

Подробнее