Spring Caching

1. Cơ Bản về Caching

Caching là gì?

  • Lưu trữ tạm thời dữ liệu
  • Giảm thời gian truy xuất
  • Giảm tải cho database
  • Tăng hiệu năng ứng dụng

Cấu hình Cơ bản

@Configuration
@EnableCaching
public class CachingConfig {

    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(
            new ConcurrentMapCache("users"),
            new ConcurrentMapCache("products"),
            new ConcurrentMapCache("orders")
        ));
        return cacheManager;
    }
}

2. Cache Annotations

@Cacheable

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Cacheable(value = "users", key = "#username")
    public User findByUsername(String username) {
        // Expensive database operation
        return userRepository.findByUsername(username)
            .orElseThrow(() -> new UserNotFoundException(username));
    }

    @Cacheable(value = "users", key = "#id", condition = "#id > 0")
    public User findById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
    }
}

@CachePut và @CacheEvict

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    @CachePut(value = "products", key = "#result.id")
    public Product updateProduct(Product product) {
        return productRepository.save(product);
    }

    @CacheEvict(value = "products", key = "#id")
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }

    @CacheEvict(value = "products", allEntries = true)
    public void clearProductCache() {
        // This method will remove all entries from the "products" cache
    }
}

3. Cache Providers

Redis Cache

@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName("localhost");
        config.setPort(6379);
        return new LettuceConnectionFactory(config);
    }

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(60))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .build();
    }
}

Caffeine Cache

@Configuration
@EnableCaching
public class CaffeineCacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(Duration.ofMinutes(5))
            .recordStats());
        return cacheManager;
    }

    @Bean
    public Caffeine caffeineConfig() {
        return Caffeine.newBuilder()
            .maximumSize(500)
            .expireAfterWrite(Duration.ofSeconds(60))
            .refreshAfterWrite(Duration.ofSeconds(30));
    }
}

4. Custom Cache Implementation

Custom Key Generator

@Component
public class CustomKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object target, Method method, Object... params) {
        StringBuilder sb = new StringBuilder();
        sb.append(target.getClass().getSimpleName());
        sb.append(".");
        sb.append(method.getName());

        for (Object param : params) {
            sb.append(".");
            sb.append(param.toString());
        }

        return sb.toString();
    }
}

Custom Cache Error Handler

@Component
public class CustomCacheErrorHandler implements CacheErrorHandler {

    private static final Logger logger = LoggerFactory.getLogger(CustomCacheErrorHandler.class);

    @Override
    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
        logger.error("Cache Get Error: " + exception.getMessage());
        // Fallback logic
    }

    @Override
    public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
        logger.error("Cache Put Error: " + exception.getMessage());
        // Fallback logic
    }

    @Override
    public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
        logger.error("Cache Evict Error: " + exception.getMessage());
        // Fallback logic
    }

    @Override
    public void handleCacheClearError(RuntimeException exception, Cache cache) {
        logger.error("Cache Clear Error: " + exception.getMessage());
        // Fallback logic
    }
}

5. Best Practices

1. Cache Strategy

@Service
public class ProductService {

    @Cacheable(value = "products", key = "#id", 
               unless = "#result == null",
               condition = "#id > 0")
    public Product getProduct(Long id) {
        return productRepository.findById(id).orElse(null);
    }

    @Cacheable(value = "products", key = "#root.methodName",
               sync = true)
    public List<Product> getAllProducts() {
        return productRepository.findAll();
    }
}

2. Cache Synchronization

@Service
public class OrderService {

    @Cacheable(value = "orders", key = "#orderId", sync = true)
    public Order getOrder(Long orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
    }

    @CachePut(value = "orders", key = "#order.id")
    @Transactional
    public Order updateOrder(Order order) {
        // Validate order exists
        getOrder(order.getId());
        return orderRepository.save(order);
    }
}

3. Cache Monitoring

@Configuration
public class CacheMonitoringConfig {

    @Bean
    public CacheMetricsRegistrar cacheMetricsRegistrar(CacheManager cacheManager,
                                                      MeterRegistry registry) {
        return new CacheMetricsRegistrar(cacheManager, registry);
    }
}

@Component
public class CacheMetricsRegistrar {

    private final CacheManager cacheManager;
    private final MeterRegistry registry;

    public CacheMetricsRegistrar(CacheManager cacheManager, MeterRegistry registry) {
        this.cacheManager = cacheManager;
        this.registry = registry;
        recordMetrics();
    }

    private void recordMetrics() {
        cacheManager.getCacheNames().forEach(cacheName -> {
            Cache cache = cacheManager.getCache(cacheName);
            if (cache instanceof ConcurrentMapCache) {
                ConcurrentMapCache concurrentCache = (ConcurrentMapCache) cache;

                Gauge.builder("cache.size", concurrentCache,
                    this::getCacheSize)
                    .tag("cache", cacheName)
                    .description("Number of entries in cache")
                    .register(registry);
            }
        });
    }

    private long getCacheSize(ConcurrentMapCache cache) {
        return cache.getNativeCache().size();
    }
}

4. Cache Warming

@Component
public class CacheWarmer implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private ProductService productService;

    @Autowired
    private CategoryService categoryService;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        warmUpCaches();
    }

    private void warmUpCaches() {
        // Warm up frequently accessed data
        productService.getAllProducts();
        categoryService.getAllCategories();

        // Warm up specific items
        List<Long> popularProductIds = Arrays.asList(1L, 2L, 3L);
        popularProductIds.forEach(id -> productService.getProduct(id));
    }
}

5. Cache Eviction Policies

```java @Configuration @EnableCaching public class EvictionConfig {

@Bean
public CacheManager cacheManager() {
    CaffeineCacheManager cacheManager = new CaffeineCacheManager();

    // Different policies for different caches
    Map<String, Caffeine<Object, Object>> configs = new HashMap<>();

    configs.put("products", Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(Duration.ofHours(1))
        .removalListener((key, value, cause) ->
            logger.info("Product cache entry removed: {} due to {}", key, cause)));

    configs.put("users", Caffeine.newBuilder()
        .maximumSize(500)
        .expireAfterAccess(Duration.ofMinutes(30))
        .removalListener((key, value, cause) ->
            logger.info("User cache entry removed: {} due to {}", key, cause)));

    cacheManager.setCaffeine(configs.get("products")); // Default config
    return cacheManager;
}

}