Redis ile Spring Boot Cache Stratejileri
Cache Neden Önemli?
Bir e-ticaret sitesi düşünün. Anasayfadaki popüler ürünler, kategori listesi, kullanıcı profili — bu veriler her istekte veritabanından sorgulanır. Günde 1 milyon istek alan bir uygulamada aynı veriyi tekrar tekrar veritabanından çekmek büyük israftır. Cache ile bu veriyi bellekte tutarak yanıt süresini milisaniyelere düşürüp veritabanı yükünü azaltabilirsiniz.
Redis, in-memory data store olarak cache için en popüler çözümdür. Spring Boot ile entegrasyonu oldukça kolaydır ve güçlü bir cache abstraction layer sunar.
Spring Boot Cache Abstraction
Spring Boot, cache mekanizmasını soyutlayarak farklı cache provider'ları (Redis, EHCache, Caffeine) aynı annotation'larla kullanmanızı sağlar.
Dependency ekleyin:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>Cache'i aktif edin:
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Redis yapılandırması:
spring:
data:
redis:
host: localhost
port: 6379
password: ""
timeout: 2000ms
cache:
type: redis
redis:
time-to-live: 3600000 # 1 saat (ms)
cache-null-values: false
key-prefix: "myapp:"
use-key-prefix: true@Cacheable, @CachePut, @CacheEvict
Spring Cache'in üç temel annotation'ı vardır:
@Cacheable — Sonucu Cache'le
@Service
public class ProductService {
private final ProductRepository productRepository;
@Cacheable(value = "products", key = "#id")
public ProductDto getProduct(Long id) {
// Bu metot ilk çağrıda DB'den okur, sonraki çağrılarda cache'den gelir
return productRepository.findById(id)
.map(this::toDto)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
}
@Cacheable(value = "products-by-category", key = "#categoryId")
public List<ProductDto> getProductsByCategory(Long categoryId) {
return productRepository.findByCategoryId(categoryId)
.stream()
.map(this::toDto)
.collect(Collectors.toList());
}
// Koşullu cache: sadece fiyatı 100'den büyük ürünleri cache'le
@Cacheable(value = "products", key = "#id", condition = "#result != null && #result.price > 100")
public ProductDto getExpensiveProduct(Long id) {
return productRepository.findById(id)
.map(this::toDto)
.orElse(null);
}
}@CachePut — Cache'i Güncelle
@CachePut(value = "products", key = "#id")
public ProductDto updateProduct(Long id, UpdateProductRequest request) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
product.setName(request.name());
product.setPrice(request.price());
return toDto(productRepository.save(product));
}@CacheEvict — Cache'den Sil
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
// Kategorideki tüm ürün cache'ini temizle
@CacheEvict(value = "products-by-category", key = "#categoryId")
public void evictCategoryCache(Long categoryId) {
// Sadece cache temizlenir
}
// Tüm product cache'ini temizle
@CacheEvict(value = "products", allEntries = true)
public void evictAllProductCache() {
// Tüm "products" cache bölgesi temizlenir
}Birden Fazla Cache İşlemi: @Caching
@Caching(
put = { @CachePut(value = "products", key = "#result.id") },
evict = { @CacheEvict(value = "products-by-category", key = "#result.categoryId") }
)
public ProductDto createProduct(CreateProductRequest request) {
Product product = productRepository.save(toEntity(request));
return toDto(product);
}TTL (Time-To-Live) Stratejileri
Farklı veriler için farklı TTL değerleri gerekebilir. Redis cache manager'ı özelleştirin:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// Varsayılan yapılandırma
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
// Cache bazında özel TTL
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
// Kategoriler: 24 saat (nadiren değişir)
cacheConfigs.put("categories", defaultConfig.entryTtl(Duration.ofHours(24)));
// Ürünler: 1 saat
cacheConfigs.put("products", defaultConfig.entryTtl(Duration.ofHours(1)));
// Kullanıcı oturumu: 30 dakika
cacheConfigs.put("user-sessions", defaultConfig.entryTtl(Duration.ofMinutes(30)));
// Arama sonuçları: 5 dakika
cacheConfigs.put("search-results", defaultConfig.entryTtl(Duration.ofMinutes(5)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
}Cache Aside Pattern
Cache Aside (Lazy Loading) en yaygın cache stratejisidir:
Uygulama önce cache'e bakar
Cache'de varsa (cache hit) direkt döner
Cache'de yoksa (cache miss) DB'den okur, cache'e yazar, sonra döner
Spring'in @Cacheable annotation'ı tam olarak bu pattern'i uygular. Manuel implementasyon:
@Service
public class ProductService {
private final RedisTemplate<String, ProductDto> redisTemplate;
private final ProductRepository productRepository;
public ProductDto getProduct(Long id) {
String cacheKey = "product:" + id;
// 1. Cache'e bak
ProductDto cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached; // Cache hit
}
// 2. DB'den oku
ProductDto product = productRepository.findById(id)
.map(this::toDto)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
// 3. Cache'e yaz
redisTemplate.opsForValue().set(cacheKey, product, Duration.ofHours(1));
return product;
}
}Write-Through ve Write-Behind
Write-Through: Veri yazılırken hem DB hem cache aynı anda güncellenir. Tutarlılık yüksek, yazma işlemi biraz yavaş.
@CachePut(value = "products", key = "#result.id")
@Transactional
public ProductDto updateProduct(Long id, UpdateProductRequest request) {
Product product = productRepository.findById(id).orElseThrow();
product.update(request);
return toDto(productRepository.save(product));
// @CachePut dönen değeri otomatik cache'e yazar
}Write-Behind (Write-Back): Veri önce cache'e yazılır, DB'ye asenkron olarak yazılır. Yazma performansı yüksek ama veri kaybı riski var.
Redis Data Structures ile İleri Cache
Redis sadece key-value store değildir. Zengin veri yapıları sunar:
@Service
public class AdvancedCacheService {
private final StringRedisTemplate redisTemplate;
// Hash: Nesne alanlarını ayrı ayrı cache'leme
public void cacheUserProfile(Long userId, Map<String, String> profile) {
String key = "user:" + userId;
redisTemplate.opsForHash().putAll(key, profile);
redisTemplate.expire(key, Duration.ofHours(2));
}
public String getUserField(Long userId, String field) {
return (String) redisTemplate.opsForHash().get("user:" + userId, field);
}
// Sorted Set: Leaderboard / sıralama
public void updateLeaderboard(String playerId, double score) {
redisTemplate.opsForZSet().add("leaderboard", playerId, score);
}
public Set<String> getTopPlayers(int count) {
return redisTemplate.opsForZSet().reverseRange("leaderboard", 0, count - 1);
}
// List: Son aktiviteler / feed
public void addActivity(Long userId, String activity) {
String key = "activities:" + userId;
redisTemplate.opsForList().leftPush(key, activity);
redisTemplate.opsForList().trim(key, 0, 99); // Son 100 aktivite
redisTemplate.expire(key, Duration.ofDays(7));
}
public List<String> getRecentActivities(Long userId, int count) {
return redisTemplate.opsForList().range("activities:" + userId, 0, count - 1);
}
// Set: Benzersiz ziyaretçi sayımı
public void trackVisitor(String pageId, String visitorId) {
redisTemplate.opsForSet().add("visitors:" + pageId, visitorId);
}
public Long getUniqueVisitorCount(String pageId) {
return redisTemplate.opsForSet().size("visitors:" + pageId);
}
}Cache Invalidation Stratejileri
Cache invalidation, bilgisayar biliminin en zor problemlerinden biridir. Stratejiler:
TTL-based: En basit. Cache belirli süre sonra otomatik expire olur.
Event-based: Veri değiştiğinde event yayınla, ilgili cache'i temizle.
Version-based: Cache key'ine versiyon numarası ekle. Versiyon değişince eski cache otomatik terk edilir.
// Event-based invalidation
@Service
public class ProductEventListener {
private final RedisCacheManager cacheManager;
@EventListener
public void onProductUpdated(ProductUpdatedEvent event) {
Cache productsCache = cacheManager.getCache("products");
if (productsCache != null) {
productsCache.evict(event.productId());
}
Cache categoryCache = cacheManager.getCache("products-by-category");
if (categoryCache != null) {
categoryCache.evict(event.categoryId());
}
}
}Best Practices
Her şeyi cache'lemeyin. Sık okunan, nadir güncellenen veriler cache'e uygundur. Sık değişen veriler cache invalidation karmaşıklığı yaratır.
Cache stampede'den korunun. Popüler bir cache key expire olduğunda yüzlerce istek aynı anda DB'ye gider. Distributed lock veya probabilistic early expiration kullanın.
Monitoring yapın. Cache hit ratio'yu izleyin. %80'in altındaysa cache stratejinizi gözden geçirin.
Serialization'a dikkat edin. JSON serialization güvenli ama yavaş. Class yapısı değiştiğinde deserialization hataları olabilir. Versiyon yönetimi yapın.
Redis memory'yi izleyin.
maxmemory-policyayarını yapılandırın.allkeys-lruçoğu senaryo için iyi bir seçimdir.
Özet
@Cacheable ile okuma cache'i, @CacheEvict ile invalidation, @CachePut ile güncelleme yapın
TTL değerlerini veri tipine göre ayarlayın — kategoriler uzun, arama sonuçları kısa
Cache Aside en yaygın pattern, Write-Through tutarlılık gerektiğinde kullanın
Redis data structures (Hash, Sorted Set, List, Set) ile gelişmiş cache senaryoları uygulayın
Cache hit ratio'yu izleyin, stampede'den korunun, memory'yi yönetin
Her şeyi cache'lemeyin — sadece sık okunan, nadir güncellenen verileri cache'leyin
Bu yazıyı beğendiniz mi?
Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.
Bu konuyu derinlemesine öğrenmek ister misin?
Spring Boot: Sıfırdan İleri Seviyeye
İlgili Yazılar
Spring Boot'ta Exception Handling: @ControllerAdvice ile Profesyonel Hata Yönetimi
Spring Boot uygulamalarında hata yönetimini profesyonel seviyeye taşıyın. @ControllerAdvice, @ExceptionHandler, custom e...
Spring Boot Nedir? Neden Spring Boot Kullanmalısınız?
Spring Boot nedir, ne işe yarar? Auto-configuration, starter dependencies, embedded server özellikleri. Java backend gel...
Spring Boot'ta Custom Annotation Yazma
Spring Boot'ta kendi annotation'ınızı yazın: meta-annotation, AOP ile cross-cutting concerns, validation ve composed ann...