← Назад к вопросам
Зачем нужен локальный кэш в микросервисной архитектуре?
1.8 Middle🔥 171 комментариев
#Кэширование и NoSQL
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Локальный кэш в микросервисной архитектуре
Локальный кэш (in-memory cache) — это важный компонент микросервисной архитектуры, который улучшает производительность и надёжность системы. Рассмотрим его назначение и применение.
Зачем нужен локальный кэш
1. Снижение латенции
Без кэша: Request → DB (50ms) → Response (50ms)
С кэшем: Request → Memory (1ms) → Response (1ms)
Улучшение в 50 раз!
public class UserService {
private final Map<String, User> cache = new ConcurrentHashMap<>();
private final UserRepository repository;
public User getUser(String id) {
// Проверяем локальный кэш первым
return cache.computeIfAbsent(id, key -> {
return repository.findById(key); // Обращение к БД только если нет в кэше
});
}
}
2. Снижение нагрузки на БД
100 микросервисов без кэша:
100 services × 1000 requests/s = 100k requests/s to DB
100 микросервисов с кэшем (80% hit rate):
100k requests × 20% = 20k requests/s to DB
Уменьшение нагрузки в 5 раз!
3. Отказоустойчивость
public class ResilientUserService {
private final UserRepository repository;
private final UserCache cache;
public User getUser(String id) {
try {
User user = repository.findById(id);
cache.put(id, user);
return user;
} catch (DatabaseException e) {
// БД недоступна, но можем вернуть закэшированное значение
User cachedUser = cache.get(id);
if (cachedUser != null) {
return cachedUser; // Сервис работает даже при падении БД
}
throw e;
}
}
}
Типы локального кэша
1. Cache-Aside (Lazy Loading)
@Service
public class CacheAsideExample {
private final ConcurrentHashMap<String, Product> cache = new ConcurrentHashMap<>();
private final ProductRepository repository;
public Product getProduct(String id) {
return cache.computeIfAbsent(id, key -> {
return repository.findById(key);
});
}
}
// Использование:
Product product = service.getProduct("123"); // Загружает в кэш
Product product2 = service.getProduct("123"); // Берёт из кэша
2. Write-Through (Синхронная запись)
@Service
public class WriteThroughExample {
private final Map<String, Product> cache = new ConcurrentHashMap<>();
private final ProductRepository repository;
public void saveProduct(Product product) {
// Сначала пишем в БД
repository.save(product);
// Потом обновляем кэш
cache.put(product.getId(), product);
}
}
3. Write-Behind (Асинхронная запись)
@Service
public class WriteBehindExample {
private final Map<String, Product> cache = new ConcurrentHashMap<>();
private final ProductRepository repository;
private final ExecutorService executor = Executors.newFixedThreadPool(5);
public void saveProduct(Product product) {
// Сразу обновляем кэш
cache.put(product.getId(), product);
// Асинхронно пишем в БД
executor.submit(() -> repository.save(product));
}
}
Реализация локального кэша
Spring Cache Abstraction
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "products", "categories");
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUser(String id) {
return repository.findById(id);
}
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return repository.save(user);
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(String id) {
repository.deleteById(id);
}
}
Google Guava Cache
@Configuration
public class GuavaCacheConfig {
@Bean
public LoadingCache<String, User> userCache(UserRepository repository) {
return CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build(new CacheLoader<String, User>() {
@Override
public User load(String key) throws Exception {
return repository.findById(key)
.orElseThrow(() -> new UserNotFoundException(key));
}
});
}
}
@Service
public class UserService {
private final LoadingCache<String, User> userCache;
public User getUser(String id) throws ExecutionException {
return userCache.get(id);
}
public void invalidateUser(String id) {
userCache.invalidate(id);
}
}
Caffeine Cache (улучшенная альтернатива Guava)
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
@Configuration
public class CaffeineConfig {
@Bean
public Cache<String, User> userCache() {
return Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.refreshAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(1000)
.recordStats()
.build();
}
}
@Service
public class UserService {
private final Cache<String, User> cache;
private final UserRepository repository;
public User getUser(String id) {
return cache.get(id, key -> repository.findById(key).orElse(null));
}
}
Стратегии инвалидации кэша
1. Time-based (TTL)
cache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES) // Время жизни
.refreshAfterWrite(2, TimeUnit.MINUTES) // Автообновление
.build();
2. Event-based (через события)
@Service
public class UserService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void updateUser(User user) {
repository.save(user);
// Публикуем событие для инвалидации кэша
eventPublisher.publishEvent(new UserUpdatedEvent(user.getId()));
}
}
@Component
public class CacheInvalidationListener {
@Autowired
private Cache<String, User> userCache;
@EventListener
public void handleUserUpdate(UserUpdatedEvent event) {
userCache.invalidate(event.getUserId());
}
}
3. Event-based через RabbitMQ (распределённая инвалидация)
@Service
public class UserService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void updateUser(User user) {
repository.save(user);
// Отправляем событие всем сервисам
rabbitTemplate.convertAndSend(
"cache-invalidation-exchange",
"user.updated",
new CacheInvalidationMessage("user", user.getId())
);
}
}
@Component
public class CacheInvalidationReceiver {
@RabbitListener(queues = "cache-invalidation-queue")
public void handleCacheInvalidation(CacheInvalidationMessage msg) {
if (msg.getCacheKey().equals("user")) {
userCache.invalidate(msg.getResourceId());
}
}
}
Практический пример: многоуровневое кэширование
@Service
public class MultiLevelCacheService {
@Autowired
private Cache<String, User> l1Cache; // Локальный
@Autowired
private StringRedisTemplate l2Cache; // Redis
@Autowired
private UserRepository repository; // БД
public User getUser(String id) {
// Level 1: Локальный кэш
User user = l1Cache.getIfPresent(id);
if (user != null) return user;
// Level 2: Redis
String cached = l2Cache.opsForValue().get("user:" + id);
if (cached != null) {
user = objectMapper.readValue(cached, User.class);
l1Cache.put(id, user);
return user;
}
// Level 3: БД
user = repository.findById(id).orElse(null);
if (user != null) {
l1Cache.put(id, user);
l2Cache.opsForValue().set("user:" + id, objectMapper.writeValueAsString(user), Duration.ofMinutes(10));
}
return user;
}
}
Best Practices
- Выбирай правильный размер — слишком большой кэш = потребление памяти
- Устанавливай TTL — старые данные должны отсекаться
- Мониторь hit rate — нужно минимум 70-80% попаданий
- Кэшируй только читаемые данные — или используй cache-aside
- Будь осторожен с синхронизацией — используй ConcurrentHashMap
- Тестируй инвалидацию — данные должны быть согласованными
- Используй Caffeine вместо Guava — это новый стандарт
- Комбинируй с Redis — для распределённых сценариев
Локальный кэш — это не серебряная пуля, но мощный инструмент для оптимизации микросервисной архитектуры!