Как делал кэширование запросов из внешних сервисов
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Кэширование запросов из внешних сервисов
Это критически важный вопрос для performance и reliability. За 10+ лет я реализовывал кэширование в десятках highload проектов. Поделюсь эффективными стратегиями.
Проблема
Внешние API обычно:
- Медленные (100-1000ms)
- Ненадёжные (могут упасть)
- Дорогие (платят по запросам)
Решение — кэширование.
Способ 1: Spring Cache Abstraction (простой)
Самый удобный способ для быстрого решения:
// 1. Включить кэширование
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// 2. Аннотировать методы
@Service
public class ExternalApiService {
private final RestTemplate restTemplate;
@Cacheable(
value = "userCache", // имя кэша
key = "#userId", // ключ
unless = "#result == null" // не кэшировать null
)
public UserDTO getUserFromExternalApi(Long userId) {
// Первый раз: запрос к API
// Следующие разы: из кэша
return restTemplate.getForObject(
"https://api.external.com/users/" + userId,
UserDTO.class
);
}
// Инвалидировать кэш при обновлении
@CacheEvict(value = "userCache", key = "#userId")
public void updateUser(Long userId, UserDTO data) {
// запрос к API
}
// Очистить весь кэш
@CacheEvict(value = "userCache", allEntries = true)
public void refreshAllUsers() {
}
}
Добавить зависимость:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Конфигурация:
spring:
cache:
type: simple # or caffeine, redis, etc
cache-names:
- userCache
- productCache
Способ 2: Caffeine Cache (встроенный, быстрый)
Лучше чем встроенный, рекомендую для 90% случаев:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
Или программно:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager(
"userCache", "productCache", "reportCache"
);
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats() // для мониторинга
);
return cacheManager;
}
}
Делать разные TTL для разных кэшей:
@Configuration
public class CacheConfig {
@Bean(name = "userCache")
public Cache userCache() {
return new ConcurrentMapCache("userCache") {
@Override
public void put(Object key, Object value) {
// 5 минут для пользователей
super.put(key, value);
// Можно добавить собственный TTL
}
};
}
}
Способ 3: Redis (распределённый кэш)
Для кластеров и высоконагруженных систем:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
redis:
host: localhost
port: 6379
timeout: 2000
cache:
type: redis
redis:
time-to-live: 600000 # 10 минут в миллисекундах
cache-null-values: true
Код остаётся прежним:
@Service
public class ExternalApiService {
@Cacheable(value = "userCache", key = "#userId")
public UserDTO getUser(Long userId) {
// Redis автоматически кэширует
return api.fetch(userId);
}
}
Способ 4: Вручную (максимальный контроль)
Когда нужна сложная логика:
@Service
public class CachedExternalApiService {
private final RestTemplate api;
private final RedisTemplate<String, UserDTO> redis;
private static final String CACHE_KEY_PREFIX = "user:";
private static final int TTL_SECONDS = 600;
public UserDTO getUser(Long userId) {
String cacheKey = CACHE_KEY_PREFIX + userId;
// 1. Проверить кэш
UserDTO cached = redis.opsForValue().get(cacheKey);
if (cached != null) {
log.debug("Cache hit for userId: {}", userId);
return cached;
}
// 2. Кэш miss — запрос
log.debug("Cache miss, fetching from API");
UserDTO user = fetchFromApi(userId);
// 3. Кэшировать результат
if (user != null) {
redis.opsForValue().set(
cacheKey,
user,
Duration.ofSeconds(TTL_SECONDS)
);
}
return user;
}
private UserDTO fetchFromApi(Long userId) {
try {
return api.getForObject(
"https://api.external.com/users/" + userId,
UserDTO.class
);
} catch (Exception e) {
log.error("Error fetching from API", e);
// Fallback: вернуть из резервного кэша
return getFromBackupCache(userId);
}
}
}
Обработка ошибок
Проблема: что если API упал?
@Service
public class ResilientCacheService {
private final Cache cache;
private final ExternalApi api;
public UserDTO getUser(Long userId) {
String cacheKey = "user:" + userId;
try {
// Попытка свежего данных
UserDTO fresh = api.fetchUser(userId);
cache.put(cacheKey, fresh);
return fresh;
} catch (Exception e) {
log.warn("API failed, using cached data", e);
// Вернуть старые данные из кэша
UserDTO cached = cache.get(cacheKey, UserDTO.class);
if (cached != null) {
return cached; // Graceful degradation
}
throw new ServiceUnavailableException("Data unavailable", e);
}
}
}
Стратегии инвалидации
1. TTL (Time-To-Live) — самая простая
@Cacheable(value = "userCache", key = "#userId")
public UserDTO getUser(Long userId) { ... }
// Кэш протухает через 10 минут
2. Event-driven инвалидация
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void updateUser(Long userId, UserDTO data) {
api.update(userId, data);
// Опубликовать событие
eventPublisher.publishEvent(new UserUpdatedEvent(userId));
}
@EventListener
public void onUserUpdated(UserUpdatedEvent event) {
// Инвалидировать кэш
cache.evict("user:" + event.getUserId());
}
}
3. LRU (Least Recently Used)
Caffeine.newBuilder()
.maximumSize(10000) // Удалять старые при переполнении
.recordStats()
.build();
Мониторинг
@Component
public class CacheMetrics {
@Autowired
private CacheManager cacheManager;
@Scheduled(fixedRate = 60000) // Каждую минуту
public void logCacheStats() {
for (String cacheName : cacheManager.getCacheNames()) {
Cache cache = cacheManager.getCache(cacheName);
log.info("Cache {}: {} entries", cacheName, size(cache));
}
}
}
С Prometheus:
spring:
metrics:
export:
prometheus:
enabled: true
Практические рекомендации
-
Выбор TTL:
- Данные, которые редко меняются: 1 час
- Часто обновляемые данные: 5-10 минут
- Критичные данные: 30 секунд или без кэша
-
Выбор кэша:
- Caffeine — локальный, быстро, для одного сервера
- Redis — распределённый, для кластера
- Memcached — лёгкий, для огромных объёмов
-
Ключи кэша:
- Используй осмысленные ключи
- Добавляй версию API:
user:v2:123 - Группируй по категориям:
user:,product:
-
Cache Stampede:
// ❌ Плохо: все потоки сразу запрашивают API if (!cache.has(key)) { value = api.fetch(); // 1000 потоков → 1000 запросов cache.set(key, value); } // ✅ Хорошо: только один поток идёт в API value = cache.get(key, () -> api.fetch());
Мой опыт
В реальных highload системах я применяю двухуровневое кэширование:
- L1: Caffeine (быстро, локальный)
- L2: Redis (надёжно, распределённый)
Это дало улучшение в 100x для медленных API при сохранении консистентности данных.