Потокобезопасный in-memory TTL-кэш для User
Реализуйте потокобезопасный in-memory кэш для типа User ниже, с ключом по User.ID (тип string), где каждая запись истекает через фиксированный TTL.
Требования:
Setсохраняет пользователя и (за)выставляет срок истечения в now + TTL.Get(id)возвращает(User, true)только для существующей не истёкшей записи; иначе(User{}, false).- Потокобезопасно для
SetиGetиз многих goroutine — без гонки данных на map. - Истёкшие записи должны освобождаться, даже если их больше не читают — ленивого удаления мало, нетронутые ключи утекут, поэтому нужна фоновая чистка.
Скелет можно расширять (поля, помощники, параметр-интервал чистки в конструкторе, метод Close для остановки чистильщика без утечки goroutine).
type User struct {
ID string
Name string
}
type Cache struct {
// ваши поля здесь
}
func NewCache(ttl time.Duration) *Cache {
// ваш код здесь
}
func (c *Cache) Set(u User) {
// ваш код здесь
}
func (c *Cache) Get(id string) (User, bool) {
// ваш код здесь
}
Допишите реализацию.
Храните записи в map с ключом User.ID, у каждой — момент истечения, под защитой sync.RWMutex. Set пишет значение и now+ttl под Lock; Get берёт RLock и считает запись с истёкшим сроком промахом. Фоновая goroutine-чистильщик выметает истёкшие ключи, чтобы нетронутые записи не оставались; метод Close останавливает её, чтобы она не утекала.
- ✗Возвращать устаревшую запись на
Get, потому что момент истечения не проверяется при чтении - ✗Читать или писать map без блокировки, считая map в Go безопасными при конкуренции
- ✗Никогда не удалять истёкшие ключи, из-за чего map растёт безгранично, хотя
Getсообщает промахи
- →Как запускать чистильщик периодически, не утекая его goroutine после удаления кэша?
- →Почему здесь
RWMutexпредпочтительнееMutexи когда этот выбор перестаёт окупаться?
Потокобезопасный TTL-кэш
Одна map хранит и значение, и момент истечения в одной записи (так не рассинхронизируются два словаря). sync.RWMutex пропускает много одновременных Get под RLock и сериализует записи под Lock.
package main
import (
"sync"
"time"
)
type User struct {
ID string
Name string
}
type entry struct {
user User
expiresAt time.Time
}
type Cache struct {
mu sync.RWMutex
data map[string]entry
ttl time.Duration
stop chan struct{}
}
func NewCache(ttl time.Duration) *Cache {
c := &Cache{
data: make(map[string]entry),
ttl: ttl,
stop: make(chan struct{}),
}
go c.janitor(ttl) // чистка с периодом TTL
return c
}
// Set записывает значение и заново выставляет срок жизни.
func (c *Cache) Set(u User) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[u.ID] = entry{user: u, expiresAt: time.Now().Add(c.ttl)}
}
// Get возвращает (User, true) только для свежей записи.
func (c *Cache) Get(id string) (User, bool) {
c.mu.RLock()
e, ok := c.data[id]
c.mu.RUnlock()
if !ok || time.Now().After(e.expiresAt) {
return User{}, false // нет ключа или срок истёк
}
return e.user, true
}
// Close останавливает фонового чистильщика, чтобы его goroutine не утекала.
func (c *Cache) Close() {
close(c.stop)
}
// janitor периодически чистит истёкшие записи, пока кэш не закрыт.
func (c *Cache) janitor(interval time.Duration) {
t := time.NewTicker(interval)
defer t.Stop()
for {
select {
case <-t.C:
c.cleanup()
case <-c.stop:
return
}
}
}
// cleanup удаляет все истёкшие записи за один проход под Lock.
func (c *Cache) cleanup() {
now := time.Now()
c.mu.Lock()
defer c.mu.Unlock()
for id, e := range c.data {
if now.After(e.expiresAt) {
delete(c.data, id)
}
}
}
Три инварианта корректности:
- Срок проверяется на чтении.
Getсравниваетtime.Now()сexpiresAtи при истечении возвращает промах — даже если фоновый чистильщик ещё не удалил ключ. - Каждый доступ под блокировкой. Запись (
Set,cleanup) — подLock, чтение (Get) — подRLock. Конкурентный доступ к map в Go без синхронизации — гонка данных иfatal error: concurrent map .... - Удаление обязательно и без утечки goroutine. Ленивого промаха на
Getмало: ключ, который больше не запрашивают, остался бы в map навсегда. Поэтомуjanitorпериодически (поtime.Ticker) вызываетcleanupподLock, аCloseзакрывает каналstop—selectловит это и завершает goroutine, чтобы она не утекала после удаления кэша.