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;
}
}