Skip to content

Caching (Önbellekleme) - Spring Boot Multi-Layer Approach

Caching, sistemlerin performansını dramtik olarak artıran en etkili optimizasyon tekniklerinden biridir. Verileri geçici olarak hızlı erişilebilir konumlarda saklayarak, pahalı hesaplama ve I/O işlemlerini minimize eder. Spring Boot, çok katmanlı caching stratejileri için kapsamlı destek sunar.

Hızlı Karar

DurumCache KullanDikkat
Veri sık okunuyor, az değişiyorEvetTTL ve invalidation net olmalı
Hesaplama pahalıEvetCache key doğru tasarlanmalı
Veri yetkiye göre değişiyorDikkatliKullanıcılar arası sızıntı riski
Yazma sonrası kesin doğrulukSınırlıStale read kabul edilmeyebilir

Üretim Kontrol Listesi

  • Problem: Cache hangi pahalı işlemi azaltıyor?
  • Çözüm: Key, TTL, invalidation, warmup ve fallback davranışı net mi?
  • Trade-off: Latency düşer; tutarsızlık, memory ve operasyon maliyeti eklenir.
  • Hata durumu: Cache stampede, hot key, stale data, eviction storm ve Redis outage ele alınmalı.
  • Ölçüm: Hit ratio, miss latency, eviction count, memory usage ve backend load izlenmeli.
  • Güvenlik/maliyet: Yetkili veri cache'lenirken tenant/user ayrımı şarttır; büyük cache bellek maliyeti üretir.

Cache Architecture Overview

Cache Patterns

Cache Pattern Açıklamaları

  1. Cache-Aside (Lazy Loading)

    • Uygulama cache'i manuel olarak yönetir
    • Cache miss durumunda otomatik data loading
    • Selective caching imkanı
  2. Write-Through

    • Cache ve database'e eşzamanlı yazma
    • Veri tutarlılığı garanti edilir
    • Her yazma işlemi için iki operasyon gerekir
  3. Write-Behind (Write-Back)

    • Cache'e hemen yazma, database'e asenkron yazma
    • Yüksek performans sağlar
    • Veri kaybı riski vardır

Distributed Cache Architecture

Application-Level Caching

Spring Cache Abstraction

Spring Cache Abstraction, caching işlemlerini declarative annotations aracılığıyla yönetmeyi sağlar. Bu yaklaşım, business logic'ten caching concern'lerini ayırarak clean code prensiplerini destekler.

Temel Özellikler:

  • @Cacheable: Methodların sonuçlarını cache'ler
  • @CacheEvict: Cache'den veri silmeye yarar
  • @CachePut: Her çağrıda cache'i günceller
  • @Caching: Birden fazla cache annotation'ını grup halinde kullanır
  • Conditional caching: Şartlı cache'leme imkanı

Cache Providers Karşılaştırması:

  • Redis: Distributed caching için ideal, persistence desteği
  • Hazelcast: In-memory data grid, clustering yetenekleri
  • Caffeine: High-performance local cache, Guava'nın gelişmiş versiyonu
  • EhCache: Hem local hem distributed cache desteği

Basic Cache Configuration

java
@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        RedisCacheManager.Builder builder = RedisCacheManager
            .RedisCacheManagerBuilder
            .fromConnectionFactory(redisConnectionFactory())
            .cacheDefaults(cacheConfiguration());
        
        return builder.build();
    }
    
    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }
}

Cache Annotations

java
@Service
public class UserService {
    
    @Cacheable(value = "users", key = "#userId")
    public User getUserById(Long userId) {
        log.info("Fetching user from database: {}", userId);
        return userRepository.findById(userId).orElse(null);
    }
    
    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        User updatedUser = userRepository.save(user);
        log.info("User updated and cached: {}", updatedUser.getId());
        return updatedUser;
    }
    
    @CacheEvict(value = "users", key = "#userId")
    public void deleteUser(Long userId) {
        userRepository.deleteById(userId);
        log.info("User deleted and evicted from cache: {}", userId);
    }
    
    @CacheEvict(value = "users", allEntries = true)
    public void clearAllUsers() {
        log.info("All users evicted from cache");
    }
}

Distributed Caching (Redis ile)

Distributed caching, multiple application instance'lar arasında paylaşılan cache verilerini yönetir. Redis, yüksek performanslı, in-memory veri yapıları sunarak distributed caching için ideal bir çözümdür.

Redis'in Avantajları

Neden Redis Kullanmalıyız?

  • High Performance: In-memory operations ile microsecond düzeyinde response time
  • Data Structures: String, Hash, List, Set, Sorted Set gibi zengin veri yapıları
  • Persistence: RDB ve AOF ile veri dayanıklılığı
  • Replication: Master-slave replication ile high availability
  • Clustering: Horizontal scaling için Redis Cluster desteği
  • Pub/Sub: Real-time messaging capabilities

Redis Deployment Options:

  • Redis Standalone: Tek instance, development için uygun
  • Redis Sentinel: Automatic failover ve monitoring
  • Redis Cluster: Horizontal partitioning ve sharding
  • Redis Streams: Event sourcing ve real-time analytics

Spring Data Redis Integration

Spring Data Redis, Redis operations için comprehensive abstraction sağlar. RedisTemplate ve ReactiveRedisTemplate ile blocking ve non-blocking operations desteklenir.

Connection Factory Seçenekleri:

  • Lettuce: Netty-based, reactive support, connection pooling
  • Jedis: Traditional blocking client, thread-safe connection pooling

Redis Configuration

java
@Configuration
public class RedisConfig {
    
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName("localhost");
        config.setPort(6379);
        config.setPassword("password");
        
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            .commandTimeout(Duration.ofSeconds(5))
            .poolConfig(connectionPoolConfig())
            .build();
            
        return new LettuceConnectionFactory(config, clientConfig);
    }
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());
        
        // JSON serialization
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        
        return template;
    }
    
    private GenericObjectPoolConfig<?> connectionPoolConfig() {
        GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        config.setMinIdle(5);
        return config;
    }
}

Manual Cache Operations

java
@Service
public class CacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public void cacheObject(String key, Object value, Duration ttl) {
        redisTemplate.opsForValue().set(key, value, ttl);
    }
    
    public <T> T getCachedObject(String key, Class<T> type) {
        Object cached = redisTemplate.opsForValue().get(key);
        return type.cast(cached);
    }
    
    public void evictFromCache(String key) {
        redisTemplate.delete(key);
    }
    
    public void cacheList(String key, List<Object> list, Duration ttl) {
        redisTemplate.opsForList().rightPushAll(key, list.toArray());
        redisTemplate.expire(key, ttl);
    }
    
    public Set<Object> getCachedSet(String key) {
        return redisTemplate.opsForSet().members(key);
    }
}

Cache Patterns ve Best Practices

Cache patterns, farklı use case'ler için optimize edilmiş caching stratejileridir. Her pattern'in kendine özgü avantaj ve dezavantajları vardır.

Cache Hierarchies

Multi-Level Cache Architecture:

  • L1 (Local Cache): Application memory'de, en hızlı erişim
  • L2 (Distributed Cache): Redis/Hazelcast, cluster-wide paylaşım
  • L3 (Database): Son çare olarak database'den okuma

Cache Coherence Strategies:

  • Write-through: Tüm cache level'lara eşzamanlı yazma
  • Write-behind: Async yazma ile performance optimizasyonu
  • Cache invalidation: Event-driven cache temizleme
  • TTL-based expiration: Zaman bazlı otomatik temizleme

Cache-Aside Pattern

Cache-Aside (Lazy Loading) pattern'de, uygulama cache'i manuel olarak yönetir. Bu en yaygın kullanılan cache pattern'idir.

Avantajlar:

  • Application'ın cache kontrolü tam elinde
  • Cache miss durumunda otomatik data loading
  • Selective caching imkanı

Dezavantajlar:

  • Cache logic business code'a karışır
  • Manual cache invalidation gerekir
  • Potential inconsistency riski
java
@Service
public class ProductService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public Product getProduct(Long productId) {
        String cacheKey = "product:" + productId;
        
        // Cache'den kontrol et
        Product cachedProduct = (Product) redisTemplate.opsForValue().get(cacheKey);
        if (cachedProduct != null) {
            return cachedProduct;
        }
        
        // Database'den getir
        Product product = productRepository.findById(productId).orElse(null);
        if (product != null) {
            // Cache'e ekle
            redisTemplate.opsForValue().set(cacheKey, product, Duration.ofMinutes(30));
        }
        
        return product;
    }
    
    public Product updateProduct(Product product) {
        // Database'i güncelle
        Product updatedProduct = productRepository.save(product);
        
        // Cache'i güncelle
        String cacheKey = "product:" + product.getId();
        redisTemplate.opsForValue().set(cacheKey, updatedProduct, Duration.ofMinutes(30));
        
        return updatedProduct;
    }
}

Write-Through Pattern

java
@Service
public class WriteThoughCacheService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public User saveUser(User user) {
        // Database'e yaz
        User savedUser = userRepository.save(user);
        
        // Cache'e yaz (write-through)
        String cacheKey = "user:" + savedUser.getId();
        redisTemplate.opsForValue().set(cacheKey, savedUser, Duration.ofHours(1));
        
        return savedUser;
    }
}

Write-Behind (Write-Back) Pattern

java
@Service
public class WriteBehindCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    private final Queue<User> writeQueue = new ConcurrentLinkedQueue<>();
    
    public User updateUser(User user) {
        // Cache'i hemen güncelle
        String cacheKey = "user:" + user.getId();
        redisTemplate.opsForValue().set(cacheKey, user, Duration.ofHours(1));
        
        // Database yazma işlemini queue'ya ekle
        writeQueue.offer(user);
        
        return user;
    }
    
    @Scheduled(fixedDelay = 5000) // 5 saniyede bir
    public void flushToDatabase() {
        List<User> usersToWrite = new ArrayList<>();
        User user;
        
        while ((user = writeQueue.poll()) != null) {
            usersToWrite.add(user);
        }
        
        if (!usersToWrite.isEmpty()) {
            userRepository.saveAll(usersToWrite);
            log.info("Flushed {} users to database", usersToWrite.size());
        }
    }
}

Multi-Level Caching

Multi-Level Cache Özellikleri

  1. L1 Cache (Local Memory)

    • En hızlı erişim süresi
    • Uygulama instance'ına özel
    • Sınırlı bellek kapasitesi
  2. L2 Cache (Distributed)

    • Cluster-wide paylaşım
    • Daha yüksek kapasite
    • Network latency etkisi
  3. Database

    • Son çare olarak kullanılır
    • Tam veri tutarlılığı
    • En yavaş erişim süresi

Cache Invalidation Strategies

Cache Invalidation Stratejileri

  1. Event-Based Invalidation

    • Veri değişikliğinde otomatik tetiklenme
    • Event-driven mimari ile entegrasyon
    • Seçici cache temizleme
  2. Time-Based Invalidation

    • TTL (Time-To-Live) bazlı temizleme
    • Otomatik cache yenileme
    • Basit ve etkili yönetim
  3. Manual Invalidation

    • Admin kontrolü
    • Acil durum müdahalesi
    • Seçici cache yönetimi

CDN Integration

Static Content Caching

java
@RestController
public class StaticContentController {
    
    @GetMapping(value = "/images/{filename}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<Resource> getImage(@PathVariable String filename) {
        try {
            Resource resource = resourceLoader.getResource("classpath:static/images/" + filename);
            
            return ResponseEntity.ok()
                .cacheControl(CacheControl.maxAge(Duration.ofDays(7))) // 7 gün cache
                .eTag(calculateETag(resource))
                .body(resource);
        } catch (Exception e) {
            return ResponseEntity.notFound().build();
        }
    }
    
    @GetMapping("/api/products/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        Product product = productService.getProduct(id);
        
        return ResponseEntity.ok()
            .cacheControl(CacheControl.maxAge(Duration.ofMinutes(10)))
            .eTag(String.valueOf(product.getLastModified().hashCode()))
            .body(product);
    }
}

API Response Caching

java
@RestController
public class CachedApiController {
    
    @GetMapping("/api/popular-products")
    @Cacheable(value = "popular-products", unless = "#result.isEmpty()")
    public ResponseEntity<List<Product>> getPopularProducts() {
        List<Product> products = productService.getPopularProducts();
        
        return ResponseEntity.ok()
            .cacheControl(CacheControl.maxAge(Duration.ofMinutes(15)))
            .body(products);
    }
    
    @GetMapping("/api/categories")
    public ResponseEntity<List<Category>> getCategories(HttpServletRequest request) {
        String etag = categoryService.getCategoriesETag();
        
        // ETag kontrolü
        if (request.getHeader("If-None-Match") != null && 
            request.getHeader("If-None-Match").equals(etag)) {
            return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
        }
        
        List<Category> categories = categoryService.getAllCategories();
        
        return ResponseEntity.ok()
            .eTag(etag)
            .cacheControl(CacheControl.maxAge(Duration.ofHours(1)))
            .body(categories);
    }
}

Performance Monitoring

Cache Metrics

java
@Component
public class CacheMetrics {
    
    private final MeterRegistry meterRegistry;
    private final Counter cacheHits;
    private final Counter cacheMisses;
    private final Timer cacheLoadTime;
    
    public CacheMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.cacheHits = Counter.builder("cache.hits")
            .tag("cache", "users")
            .register(meterRegistry);
        this.cacheMisses = Counter.builder("cache.misses")
            .tag("cache", "users")
            .register(meterRegistry);
        this.cacheLoadTime = Timer.builder("cache.load.time")
            .register(meterRegistry);
    }
    
    public void recordCacheHit(String cacheName) {
        cacheHits.increment(Tags.of("cache", cacheName));
    }
    
    public void recordCacheMiss(String cacheName) {
        cacheMisses.increment(Tags.of("cache", cacheName));
    }
    
    public void recordCacheLoadTime(Duration duration) {
        cacheLoadTime.record(duration);
    }
}

Cache Health Monitoring

java
@Component
public class CacheHealthIndicator implements HealthIndicator {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Override
    public Health health() {
        try {
            redisTemplate.opsForValue().set("health-check", "ping", Duration.ofSeconds(10));
            String response = (String) redisTemplate.opsForValue().get("health-check");
            
            if ("ping".equals(response)) {
                return Health.up()
                    .withDetail("redis", "Available")
                    .withDetail("responseTime", measureResponseTime() + "ms")
                    .build();
            } else {
                return Health.down()
                    .withDetail("redis", "Invalid response")
                    .build();
            }
        } catch (Exception e) {
            return Health.down()
                .withDetail("redis", "Unavailable")
                .withException(e)
                .build();
        }
    }
    
    private long measureResponseTime() {
        long start = System.currentTimeMillis();
        redisTemplate.opsForValue().get("health-check");
        return System.currentTimeMillis() - start;
    }
}

Production Best Practices

Cache Warming

java
@Component
public class CacheWarmupService {
    
    @Autowired
    private ProductService productService;
    
    @Autowired
    private UserService userService;
    
    @EventListener(ApplicationReadyEvent.class)
    public void warmupCache() {
        log.info("Starting cache warmup...");
        
        CompletableFuture.runAsync(this::warmupPopularProducts);
        CompletableFuture.runAsync(this::warmupFrequentlyAccessedUsers);
        
        log.info("Cache warmup initiated");
    }
    
    private void warmupPopularProducts() {
        List<Long> popularProductIds = getPopularProductIds();
        popularProductIds.forEach(productService::getProduct);
        log.info("Warmed up {} popular products", popularProductIds.size());
    }
    
    private void warmupFrequentlyAccessedUsers() {
        List<Long> frequentUserIds = getFrequentlyAccessedUserIds();
        frequentUserIds.forEach(userService::getUserById);
        log.info("Warmed up {} frequently accessed users", frequentUserIds.size());
    }
}

Cache Disaster Recovery

java
@Service
public class CacheDisasterRecoveryService {
    
    @Autowired
    private RedisTemplate<String, Object> primaryRedis;
    
    @Autowired
    private RedisTemplate<String, Object> backupRedis;
    
    @Retryable(value = Exception.class, maxAttempts = 3)
    public Object getCachedValue(String key) {
        try {
            return primaryRedis.opsForValue().get(key);
        } catch (Exception e) {
            log.warn("Primary Redis failed, trying backup: {}", e.getMessage());
            return backupRedis.opsForValue().get(key);
        }
    }
    
    @Async
    public void syncCaches() {
        Set<String> keys = primaryRedis.keys("*");
        for (String key : keys) {
            try {
                Object value = primaryRedis.opsForValue().get(key);
                Long ttl = primaryRedis.getExpire(key);
                
                if (ttl > 0) {
                    backupRedis.opsForValue().set(key, value, Duration.ofSeconds(ttl));
                }
            } catch (Exception e) {
                log.error("Failed to sync cache key {}: {}", key, e.getMessage());
            }
        }
    }
}

Bu caching stratejileri, sistem performansını önemli ölçüde artırır ve database yükünü azaltır. Redis ile distributed caching, multi-level cache yapıları ve proper invalidation stratejileri ile production-ready caching solutions sağlar.

Cache-Through ve Database-Through Terminolojisi

“Cache-through” tek bir resmi pattern adı gibi kullanılmamalıdır; akış açıkça adlandırılmalıdır:

PatternOkuma/yazma akışıSource of truth
Cache-asideApplication cache miss'te DB'den okur, cache'i doldururDB
Read-throughCache provider miss'te loader ile DB'yi çağırırDB
Write-throughYazma cache üzerinden DB'ye senkron geçerDB
Write-behindÖnce cache'e yazar, DB'yi asenkron güncellerDB; gecikmeli
Direct DB / DB-throughCache bypass edilip DB'den okunur/yazılırDB

Cache bir hız katmanıdır; iş doğruluğunun kaynağı değilse database source of truth olarak kalmalıdır. Cache invalidation, TTL, stale read, stampede, hot key ve failover davranışı seçilen pattern ile birlikte belgelenir.

Pattern Seçim Kontrolü

  • Cache miss sırasında DB çağrısını hangi katman yapıyor?
  • DB başarılı olmadan cache response'u kullanıcıya döner mi?
  • Cache write başarısızsa mutation başarısız mı sayılır?
  • Duplicate write ve retry idempotent mi?
  • Cache tamamen kaybolduğunda origin kapasitesi yeterli mi?

Eren Demir tarafından oluşturulmuştur.