Сделайте декоратор прежнего KVDatabase: Get читает локальный TTL-кэш, Keys делегируется источнику, а MGet объединяет попадания с одним пакетным запросом по промахам.
Локальный кэш для удалённого KV‑хранилища
Сделайте декоратор прежнего 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] = ©
} 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]] = ©
d.put(missing[j], copy)
}
return result, nil
}
Объём ограничен max, вытеснение произвольное, ошибки и отсутствие не кэшируются. Это допускает устаревание до TTL и параллельные повторные загрузки; при необходимости добавьте объединение промахов и версии значений. Keys нельзя подменить списком локальной map: она неполна. Возвращаемые строки копируются, чтобы внешний код не менял содержимое кэша через *string.