Локальный кэш для удалённого KV‑хранилища

Сделайте декоратор прежнего KVDatabase: Get читает локальный TTL-кэш, Keys делегируется источнику, а MGet объединяет попадания с одним пакетным запросом по промахам.

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

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

Сделайте декоратор прежнего KVDatabase: Get читает локальный TTL-кэш, Keys делегируется источнику, а MGet объединяет попадания с одним пакетным запросом по промахам.

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

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

Условие

Необходимо реализовать локальный кэш, который будет скрытно оборачивать работу с удалённой key‑value базой (ключ — идентификатор пользователя, значение — его IPv4‑адрес). Цель — сократить время доступа (примерно 100 мс) за счёт локального хранения часто запрашиваемых данных. Пользовательский код взаимодействует с базой через прежний интерфейс, не подозревая о наличии кэша; вся логика кэширования должна быть инкапсулирована внутри реализации.

type KVDatabase interface {
  Get(key string) (string, error)
  Keys() ([]string, error)
  MGet(keys []string) ([]*string, error)
}

Ответ

Сделайте декоратор прежнего KVDatabase: Get читает локальный TTL-кэш, Keys делегируется источнику, а MGet объединяет попадания с одним пакетным запросом по промахам.

Пример предполагает, что MGet возвращает значения в порядке ключей, nil для отсутствующего ключа. Нужны fmt, sync и time:

type cacheEntry struct { value string; expires time.Time }
type CacheDB struct {
    inner KVDatabase
    ttl time.Duration
    max int
    mu sync.Mutex
    items map[string]cacheEntry
}
func NewCacheDB(inner KVDatabase, ttl time.Duration, max int) *CacheDB {
    if ttl <= 0 || max < 1 { panic("invalid cache settings") }
    return &CacheDB{inner: inner, ttl: ttl, max: max,
        items: make(map[string]cacheEntry)}
}
func (d *CacheDB) lookup(key string) (string, bool) {
    d.mu.Lock()
    defer d.mu.Unlock()
    e, ok := d.items[key]
    if ok && time.Now().Before(e.expires) { return e.value, true }
    delete(d.items, key)
    return "", false
}
func (d *CacheDB) put(key, value string) {
    d.mu.Lock()
    defer d.mu.Unlock()
    if len(d.items) >= d.max {
        for k := range d.items { delete(d.items, k); break }
    }
    d.items[key] = cacheEntry{value, time.Now().Add(d.ttl)}
}
func (d *CacheDB) Get(key string) (string, error) {
    if value, ok := d.lookup(key); ok { return value, nil }
    value, err := d.inner.Get(key)
    if err == nil { d.put(key, value) }
    return value, err
}
func (d *CacheDB) Keys() ([]string, error) { return d.inner.Keys() }
func (d *CacheDB) MGet(keys []string) ([]*string, error) {
    result := make([]*string, len(keys))
    missing := make([]string, 0)
    positions := make([]int, 0)
    for i, key := range keys {
        if value, ok := d.lookup(key); ok {
            copy := value
            result[i] = &copy
        } else {
            missing = append(missing, key)
            positions = append(positions, i)
        }
    }
    if len(missing) == 0 { return result, nil }
    loaded, err := d.inner.MGet(missing)
    if err != nil { return nil, err }
    if len(loaded) != len(missing) { return nil, fmt.Errorf("invalid MGet result") }
    for j, value := range loaded {
        if value == nil { continue }
        copy := *value
        result[positions[j]] = &copy
        d.put(missing[j], copy)
    }
    return result, nil
}

Объём ограничен max, вытеснение произвольное, ошибки и отсутствие не кэшируются. Это допускает устаревание до TTL и параллельные повторные загрузки; при необходимости добавьте объединение промахов и версии значений. Keys нельзя подменить списком локальной map: она неполна. Возвращаемые строки копируются, чтобы внешний код не менял содержимое кэша через *string.

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

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

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

Подробнее