package com.krylov.refound.service.redis; import com.krylov.refound.dto.CachedPostPage; import com.krylov.refound.dto.MapMarkerDto; import java.time.Duration; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; @Service @RequiredArgsConstructor @Slf4j public class PostCacheService { // Общая версия ВСЕХ данных posts. От неё зависят и лента, и карта — // любое изменение поста должно инвалидировать оба представления разом. // Ключ без TTL. Redis должен работать с maxmemory-policy volatile-lru, // чтобы этот ключ не вытеснялся private static final String VERSION_KEY = "posts:version"; private final RedisTemplate feedRedisTemplate; private final RedisTemplate> mapRedisTemplate; private final StringRedisTemplate stringRedisTemplate; public CachedPostPage getFeed(String key) { try { return feedRedisTemplate.opsForValue().get(key); } catch (Exception e) { // Кэш best-effort: любая проблема (Redis недоступен, битый JSON) = miss log.warn("Не удалось прочитать feed из Redis, key={}: {}", key, e.toString()); return null; } } public void saveFeed(String key, CachedPostPage value, Duration ttl) { try { feedRedisTemplate.opsForValue().set(key, value, ttl); } catch (Exception e) { log.warn("Не удалось сохранить feed в Redis, key={}: {}", key, e.toString()); } } public List getMap(String key) { try { return mapRedisTemplate.opsForValue().get(key); } catch (Exception e) { log.warn("Не удалось прочитать map из Redis, key={}: {}", key, e.toString()); return null; } } public void saveMap(String key, List value, Duration ttl) { try { mapRedisTemplate.opsForValue().set(key, value, ttl); } catch (Exception e) { log.warn("Не удалось сохранить map в Redis, key={}: {}", key, e.toString()); } } /** Текущая версия данных posts. Должна читаться ДО запроса в БД, в обоих сервисах. */ public String currentVersion() { try { String version = stringRedisTemplate.opsForValue().get(VERSION_KEY); return version == null ? "0" : version; } catch (Exception e) { log.warn("Не удалось прочитать версию posts cache: {}", e.toString()); return "0"; } } /** * Инвалидирует ВСЕ производные кэши (ленту и карту) после коммита текущей транзакции * (или сразу, если транзакции нет). */ public void invalidatePosts() { if (TransactionSynchronizationManager.isSynchronizationActive()) { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { bumpVersion(); } }); } else { bumpVersion(); } } private void bumpVersion() { try { Long version = stringRedisTemplate.opsForValue().increment(VERSION_KEY); log.info("Posts cache инвалидирован (лента + карта), версия={}", version); } catch (Exception e) { // Устаревшие записи доживут до TTL log.warn("Не удалось инвалидировать posts cache: {}", e.toString()); } } }