Как реализовать кэширование абстрактно?
Что проверяют
Умеете ли вы отделить идею кэша от конкретной реализации: интерфейс, политика вытеснения, срок жизни, поведение под конкурентной нагрузкой. Следом обычно просят написать LRU на коленке или спрашивают, как кэш узнаёт, что данные в базе изменились.
Как отвечать
Начните с контракта, а не с HashMap. Кэш это интерфейс Cache<K, V> с get, put, invalidate и чаще всего get(key, loader), а вызывающий код не знает, что внутри. Дальше разберите измерения:
- размещение: локальный в памяти процесса (
ConcurrentHashMap, Caffeine) или внешний (Redis). Локальный быстрее, но у каждого инстанса своя копия; - вытеснение: LRU выкидывает то, к чему дольше всего не обращались, LFU то, к чему обращались реже всего. Caffeine использует W-TinyLFU, который обычно выигрывает у обоих;
- срок жизни:
expireAfterWriteпротивexpireAfterAccess, плюс явная инвалидация при изменении данных; - стратегия чтения и записи: cache-aside (приложение само смотрит в кэш и ходит в источник), read-through, write-through, write-behind.
Учебный LRU пишется на LinkedHashMap с accessOrder = true и переопределённым removeEldestEntry. Сразу оговорите, что он не потокобезопасен: даже get в режиме порядка доступа меняет внутренний список, так что обёртка Collections.synchronizedMap превращает кэш в узкое место.
Для Spring назовите готовую абстракцию: @EnableCaching, @Cacheable, @CacheEvict, интерфейсы CacheManager и Cache. Провайдер подключается отдельно, Caffeine или Redis, а код сервиса не меняется. Это и есть «абстрактное» кэширование, построенное на прокси.
Где спотыкаются
Пишут if (!map.containsKey(k)) map.put(k, load(k)). Между проверкой и записью другой поток успевает сделать то же, и дорогая загрузка идёт несколько раз. Нужен атомарный computeIfAbsent или загрузчик кэша.
Забывают про ограничение размера. Кэш без лимита и TTL это медленная утечка памяти, которая заканчивается OutOfMemoryError.
Вешают @Cacheable на метод и вызывают его из того же класса. Вызов идёт мимо прокси, и кэш молча не работает.