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.
Cache Architecture Overview
Cache Patterns
Cache Pattern Açıklamaları
Cache-Aside (Lazy Loading)
- Uygulama cache'i manuel olarak yönetir
- Cache miss durumunda otomatik data loading
- Selective caching imkanı
Write-Through
- Cache ve database'e eşzamanlı yazma
- Veri tutarlılığı garanti edilir
- Her yazma işlemi için iki operasyon gerekir
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
@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
@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
@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
@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
@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
@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
@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
L1 Cache (Local Memory)
- En hızlı erişim süresi
- Uygulama instance'ına özel
- Sınırlı bellek kapasitesi
L2 Cache (Distributed)
- Cluster-wide paylaşım
- Daha yüksek kapasite
- Network latency etkisi
Database
- Son çare olarak kullanılır
- Tam veri tutarlılığı
- En yavaş erişim süresi
Cache Invalidation Strategies
Cache Invalidation Stratejileri
Event-Based Invalidation
- Veri değişikliğinde otomatik tetiklenme
- Event-driven mimari ile entegrasyon
- Seçici cache temizleme
Time-Based Invalidation
- TTL (Time-To-Live) bazlı temizleme
- Otomatik cache yenileme
- Basit ve etkili yönetim
Manual Invalidation
- Admin kontrolü
- Acil durum müdahalesi
- Seçici cache yönetimi
CDN Integration
Static Content Caching
@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
@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
@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
@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
@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
@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.