Декоратор проверяет локальный TTL-кэш перед удалённым Get и сохраняет успешный ответ. Совпадающие промахи стоит объединять, а размер кэша ограничивать.
Локальный кэш для снижения латентности при работе с удалённым key‑value хранилищем
Декоратор проверяет локальный 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.