Как реализовать конкурентное in-memory хранилище с TTL для записей в Go?
Что проверяют
Умеете ли вы совместить синхронизацию, фоновую горутину и её корректную остановку. Map с мьютексом пишут все, отличает ответ то, как вы удаляете просроченное и не течёт ли горутина. Следом спрашивают, как это тестировать без time.Sleep.
Как отвечать
Каркас:
- структура:
Store[K comparable, V any]сsync.RWMutexиmap[K]entry[V], гдеentryхранит значение иexpiresAt time.Time. Дженерики вместоinterface{}избавляют от приведения типов; - ленивое истечение:
GetподRLockпроверяетtime.Now().After(e.expiresAt)и не отдаёт просроченное. Удалить подRLockнельзя: нужно отпустить его, взятьLockи перепроверить запись, её мог перезаписать другойSet; - фоновая чистка: горутина с
time.NewTickerраз в интервал удаляет истёкшие записи, иначе ключи, которые никто не читает, живут вечно; - остановка: конструктор принимает
context.Contextили у хранилища естьClose(), закрывающий каналdone. Горутина выходит поselect, тикер останавливается черезdefer ticker.Stop(), повторныйCloseзащищёнsync.Once; - монотонное время:
time.Now()несёт показания монотонных часов, поэтомуAfterне ломается при переводе системных часов.
Полный обход map под Lock на миллионе ключей блокирует все операции. Варианты: чистить за проход случайную выборку ключей, как делает Redis, держать мин-кучу по времени истечения на container/heap или разбить хранилище на шарды с отдельной чисткой.
Для тестов время внедряется функцией now func() time.Time, и истечение проверяется сдвигом фейковых часов. В свежих версиях Go для этого есть пакет testing/synctest. Всё гоняется с go test -race.
Где спотыкаются
Запускают чистку горутиной без способа её остановить. Каждый экземпляр из тестов оставляет висящую горутину, и утечку видно по росту их числа.
Считают, что Get, который возвращает «не найдено» для просроченной записи, решает задачу. Память при этом не освобождается, пока запись не удалит кто-то ещё.
Держат Lock во время посторонней работы, например вызывают колбэк удаления под мьютексом. Колбэк обращается к хранилищу, мьютекс в Go не реентерабельный, и получается взаимная блокировка.