← Назад к вопросам

Зачем нужен локальный кэш в микросервисной архитектуре?

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

  1. Выбирай правильный размер — слишком большой кэш = потребление памяти
  2. Устанавливай TTL — старые данные должны отсекаться
  3. Мониторь hit rate — нужно минимум 70-80% попаданий
  4. Кэшируй только читаемые данные — или используй cache-aside
  5. Будь осторожен с синхронизацией — используй ConcurrentHashMap
  6. Тестируй инвалидацию — данные должны быть согласованными
  7. Используй Caffeine вместо Guava — это новый стандарт
  8. Комбинируй с Redis — для распределённых сценариев

Локальный кэш — это не серебряная пуля, но мощный инструмент для оптимизации микросервисной архитектуры!