Caching is essential for improving performance in content management systems. Halo provides a flexible caching framework built on Spring’s caching abstraction, allowing you to cache rendered pages and data with minimal configuration.
Overview
Halo uses Spring Framework’s Caching abstraction, which provides a unified interface for various cache implementations. This design allows you to switch cache providers with minimal code changes.
Benefits:
- Reduce database queries for frequently accessed data
- Improve page load times for static or semi-static content
- Lower server resource usage
- Easy provider switching (in-memory, Redis, etc.)
Cache Configuration
Halo provides CacheProperties for enabling/disabling caches:
halo:
caches:
page:
disabled: false # Enable page caching
others:
disabled: false # Enable other caches
Configuration Location: application.yaml or application.properties
Disabling Caches
For development or debugging, you may want to disable caching:
halo:
caches:
page:
disabled: true # Disable page caching
others:
disabled: true # Disable other caches
Disabling caches in production environments will significantly impact performance. Only disable caches for debugging purposes.
Page Caching
Page caching stores rendered HTML pages to avoid repeated template rendering.
Page Cache Rules
Pages are cached only when ALL conditions are met:
- Request method is
GET
- Response status is
HTTP 200 (OK)
- Content-Type is
text/html
- Content is rendered by template engine
Page Cache Configuration
| Property | Value |
|---|
| Cache Name | page |
| Eviction Policy | Time-based (since last access) |
| TTL | 1 hour from last access |
| Maximum Entries | 10,000 entries |
Page cache uses an LRU (Least Recently Used) eviction policy when the maximum number of entries is reached.
Example: Cached Pages
The following pages are typically cached:
# Homepage
GET https://example.com/
# Post detail pages
GET https://example.com/posts/hello-world
# Category pages
GET https://example.com/categories/technology
# Tag pages
GET https://example.com/tags/tutorial
# Archive pages
GET https://example.com/archives/2024/03
Pages NOT Cached
These requests are NOT cached:
# POST requests
POST https://example.com/api/posts
# API responses (JSON)
GET https://example.com/api/v1alpha1/posts
# Error pages
GET https://example.com/non-existent-page # 404
# Admin console
GET https://example.com/console/*
Custom Caching in Plugins
You can use Spring’s caching annotations in your plugins:
Using @Cacheable
Cache method results:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
public class ProductService {
private final ReactiveExtensionClient client;
public ProductService(ReactiveExtensionClient client) {
this.client = client;
}
@Cacheable(value = "products", key = "#name")
public Mono<Product> getProduct(String name) {
return client.fetch(Product.class, name);
}
@Cacheable(value = "product-list", key = "#category")
public Flux<Product> getProductsByCategory(String category) {
return client.list(Product.class,
product -> category.equals(product.getSpec().getCategory()),
null
);
}
}
Using @CachePut
Update cache entries:
import org.springframework.cache.annotation.CachePut;
@Service
public class ProductService {
@CachePut(value = "products", key = "#product.metadata.name")
public Mono<Product> updateProduct(Product product) {
return client.update(product);
}
}
Using @CacheEvict
Remove cache entries:
import org.springframework.cache.annotation.CacheEvict;
@Service
public class ProductService {
@CacheEvict(value = "products", key = "#name")
public Mono<Void> deleteProduct(String name) {
return client.delete(Product.class, name);
}
// Clear entire cache
@CacheEvict(value = "products", allEntries = true)
public Mono<Void> clearAllProducts() {
return Mono.empty();
}
}
Using @Caching
Combine multiple cache operations:
import org.springframework.cache.annotation.Caching;
@Service
public class ProductService {
@Caching(
put = {
@CachePut(value = "products", key = "#product.metadata.name")
},
evict = {
@CacheEvict(value = "product-list", allEntries = true),
@CacheEvict(value = "featured-products", allEntries = true)
}
)
public Mono<Product> publishProduct(Product product) {
product.getSpec().setPublished(true);
return client.update(product);
}
}
Cache Configuration for Plugins
Define custom cache configurations:
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.benmanes.caffeine.cache.Caffeine;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CaffeineCacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager(
"products",
"product-list",
"featured-products"
);
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(30))
.recordStats()
);
return cacheManager;
}
}
Cache with Custom Key Generator
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
@Configuration
public class CacheConfig {
@Bean
public KeyGenerator customKeyGenerator() {
return (target, method, 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();
};
}
}
@Service
public class ProductService {
@Cacheable(value = "products", keyGenerator = "customKeyGenerator")
public Mono<Product> getProduct(String name, String version) {
return client.fetch(Product.class, name);
}
}
Cache Providers
In-Memory Cache (Default)
Halo uses Caffeine as the default in-memory cache:
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=10000,expireAfterWrite=3600s
Pros:
- Fast access
- No external dependencies
- Simple configuration
Cons:
- Not shared across instances
- Lost on restart
- Memory limited
Redis Cache
For distributed deployments, use Redis:
spring:
cache:
type: redis
redis:
host: localhost
port: 6379
password: yourpassword
database: 0
Configuration Class:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()
)
)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
Pros:
- Shared across instances
- Persistent (with AOF/RDB)
- Scalable
- Support for advanced features
Cons:
- External dependency
- Network latency
- More complex setup
Use Redis cache for production deployments with multiple Halo instances to ensure cache consistency.
Cache Invalidation Strategies
Time-Based Expiration
@Cacheable(value = "posts", key = "#name")
public Mono<Post> getPost(String name) {
return client.fetch(Post.class, name);
}
With configuration:
spring:
cache:
caffeine:
spec: expireAfterWrite=1h
Event-Based Invalidation
import org.springframework.context.event.EventListener;
import org.springframework.cache.annotation.CacheEvict;
import run.halo.app.event.post.PostUpdatedEvent;
@Component
public class PostCacheInvalidator {
@EventListener(PostUpdatedEvent.class)
@CacheEvict(value = "posts", key = "#event.postName")
public void onPostUpdated(PostUpdatedEvent event) {
// Cache automatically evicted
}
@EventListener(PostUpdatedEvent.class)
@CacheEvict(value = "post-list", allEntries = true)
public void onPostListChanged(PostUpdatedEvent event) {
// Clear entire post list cache
}
}
Manual Invalidation
import org.springframework.cache.CacheManager;
@Service
public class CacheService {
private final CacheManager cacheManager;
public void clearCache(String cacheName) {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.clear();
}
}
public void evictCacheEntry(String cacheName, Object key) {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.evict(key);
}
}
}
Enable Statistics
@Bean
public CaffeineCacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(30))
.recordStats() // Enable statistics
);
return manager;
}
Access Cache Statistics
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
@Service
public class CacheMonitorService {
private final CacheManager cacheManager;
public CacheStats getCacheStats(String cacheName) {
var cache = cacheManager.getCache(cacheName);
if (cache instanceof CaffeineCache caffeineCache) {
Cache<Object, Object> nativeCache = caffeineCache.getNativeCache();
return nativeCache.stats();
}
return null;
}
public Map<String, Object> getCacheMetrics(String cacheName) {
CacheStats stats = getCacheStats(cacheName);
if (stats == null) {
return Map.of();
}
return Map.of(
"hitCount", stats.hitCount(),
"missCount", stats.missCount(),
"hitRate", stats.hitRate(),
"evictionCount", stats.evictionCount(),
"loadSuccessCount", stats.loadSuccessCount(),
"loadFailureCount", stats.loadFailureCount()
);
}
}
Best Practices
- Cache Appropriate Data: Cache data that is expensive to compute or fetch
- Set TTL Values: Always set appropriate time-to-live values
- Monitor Hit Rates: Track cache hit rates to optimize cache strategy
- Use Cache Keys Wisely: Design cache keys to avoid collisions
- Handle Cache Failures: Implement fallback when cache is unavailable
- Clear Stale Data: Implement proper invalidation strategies
- Size Limitations: Set maximum cache sizes to prevent memory issues
- Test Without Cache: Ensure application works when cache is disabled
Cache should improve performance, not correctness. Your application must work correctly even when caching is disabled.