İçeriğe geç

Redis ile Spring Boot Cache Stratejileri

T
Tolgahan
· · 14 dk okuma · 258 görüntülenme

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:

  1. Uygulama önce cache'e bakar

  2. Cache'de varsa (cache hit) direkt döner

  3. 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:

  1. TTL-based: En basit. Cache belirli süre sonra otomatik expire olur.

  2. Event-based: Veri değiştiğinde event yayınla, ilgili cache'i temizle.

  3. 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

  1. 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.

  2. 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.

  3. Monitoring yapın. Cache hit ratio'yu izleyin. %80'in altındaysa cache stratejinizi gözden geçirin.

  4. 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.

  5. Redis memory'yi izleyin. maxmemory-policy ayarı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

Paylaş:
Son güncelleme: Jun 05, 2026

Yorumlar

Giriş yapın ve yorum bırakın.

Henüz yorum yok

Düşüncelerinizi paylaşan ilk siz olun!

Bu yazıyı beğendiniz mi?

Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.

İlgili Yazılar