Unified Caching Interface Supporting Multiple Storage Backends
Bus Cache is an enterprise-level caching framework that provides a unified caching abstraction supporting multiple storage implementations. It offers seamless integration with various cache providers while maintaining a consistent API, enabling developers to switch between different caching technologies without code changes.
The framework features declarative caching through annotations, automatic cache key generation, cache penetration prevention, and comprehensive metrics monitoring.
- Provider Agnostic: Single API for all cache implementations (Redis, Caffeine, Memcached, etc.)
- Zero Migration Cost: Switch between cache providers by configuration only
- Type-Safe Generics: Full support for generic key-value pairs with compile-time safety
- Batch Operations: Optimized bulk read/write operations for improved performance
| Implementation | Use Case | Performance |
|---|---|---|
| MemoryCache | Local in-memory caching | |
| CaffeineCache | High-performance local cache | Hit Rate |
| GuavaCache | Guava-based local cache | Hit Rate |
| RedisCache | Distributed single-node Redis | Network Latency |
| RedisClusterCache | Distributed Redis Cluster | Network Latency |
| MemcachedCache | Memcached distributed cache | Network Latency |
| NoOpCache | Testing/No-op cache | N/A |
- @Cached: Read-through caching with automatic write-on-miss
- @CachedGet: Read-only cache access without automatic updates
- @Invalid: Cache invalidation based on method execution
- @CacheKey: Custom cache key generation using SpEL expressions
- Cache Penetration Prevention: Automatic placeholder insertion for null results
- SpEL Support: Dynamic cache key generation using Spring Expression Language
- Conditional Caching: Cache based on runtime conditions with SpEL
- Flexible Expiration: Per-entry or global TTL configuration, with
CacheExpire.FOREVER(0) meaning never expire - TTL Refresh:
renew(key, expire)extends the TTL of an existing entry without changing the key - Atomic Counters:
increment(key)provides monotonic counters for rate limits, sequences, and lightweight metrics - Statistics Integration: Built-in cache hit rate statistics and monitoring
- Multi-Key Caching: Batch operations with collection-based cache keys
<dependency>
<groupId>org.miaixz</groupId>
<artifactId>bus-cache</artifactId>
<version>x.x.x</version>
</dependency><dependency>
<groupId>org.miaixz</groupId>
<artifactId>bus-starter</artifactId>
<version>x.x.x</version>
</dependency>CacheProperties (bus.cache.*) supports the following fields:
| Property | Type | Description |
|---|---|---|
type |
String |
Fully-qualified class name of the Collector implementation |
prefix |
String |
Global cache key prefix applied to all keys |
timeout |
String |
Default expiration timeout (e.g. "3600000", mainly used by Redis) |
provider.url |
String |
JDBC URL for database-backed Collector (MySQL, H2, SQLite, etc.) |
provider.username |
String |
Database username |
provider.password |
String |
Database password |
Cache instances (MemoryCache, CaffeineCache, RedisCache, etc.) are interface types and must be registered as
Spring Beans — they cannot be bound from YAML.
Example: in-memory collector (no persistence)
bus:
cache:
type: org.miaixz.bus.cache.collect.MemoryCollector
prefix: myappExample: MySQL-backed collector (survives restarts)
bus:
cache:
type: org.miaixz.bus.cache.collect.MySQLCollector
prefix: myapp
provider:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: secretExample: bus-metrics adapter (Prometheus / Micrometer / OTel)
bus:
cache:
type: org.miaixz.bus.metrics.builtin.CacheMetricsAdapter
prefix: myapp@SpringBootApplication
@EnableCache
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public CacheX<String, Object> memoryCache() {
return new MemoryCache<>(1000, 180000);
}
@Bean
public CacheX<String, Object> caffeineCache() {
return new CaffeineCache<>(10000, 3600000);
}
@Bean
public CacheX<String, Object> redisCache(JedisPool jedisPool) {
return new RedisCache<>(jedisPool, new Hessian2Serializer());
}
}@Service
public class UserService {
private final CacheX<String, User> cache;
public UserService() {
// Create a local memory cache
this.cache = new MemoryCache<>(1000, 180000);
}
public User getUser(String userId) {
// Try to read from cache
User user = cache.read(userId);
if (user == null) {
// Cache miss - fetch from database
user = userRepository.findById(userId);
// Write to cache with 30-minute expiration
cache.write(userId, user, 30 * 60 * 1000);
}
return user;
}
public void updateUser(String userId, User user) {
// Update database
userRepository.save(user);
// Update cache
cache.write(userId, user, 30 * 60 * 1000);
}
public void deleteUser(String userId) {
// Delete from database
userRepository.deleteById(userId);
// Remove from cache
cache.remove(userId);
}
}@Service
public class ProductService {
private final CacheX<String, Product> cache;
public ProductService() {
// High-performance Caffeine cache
this.cache = new CaffeineCache<>(
10000, // Maximum 10,000 entries
60 * 60 * 1000 // 1-hour expiration
);
}
public Product getProduct(String productId) {
return cache.read(productId);
}
public Map<String, Product> getProducts(List<String> productIds) {
// Batch read - optimized for performance
return cache.read(productIds);
}
public void cacheProducts(Map<String, Product> products) {
// Batch write
cache.write(products, 60 * 60 * 1000);
}
}@Service
public class OrderService {
@Cached(name = "redis", prefix = "order:", expire = 30 * 60 * 1000)
public Order getOrderById(String orderId) {
// This method's result is cached
// Cache key: order:{orderId}
return orderRepository.findById(orderId);
}
@Cached(
name = "caffeine",
prefix = "user:",
expire = 60 * 60 * 1000,
condition = "#userId != null"
)
public User getUserById(String userId) {
// Only cached if userId is not null
// Cache key: user:{userId}
return userRepository.findById(userId);
}
}@Service
public class ConfigurationService {
@CachedGet(name = "memory", prefix = "config:")
public String getConfig(String key) {
// Read from cache without automatic write on miss
return configRepository.findByKey(key);
}
}@Service
public class UserService {
@Cached(name = "redis", prefix = "user:", expire = 60 * 60 * 1000)
public User getUser(String userId) {
return userRepository.findById(userId);
}
@Invalid(name = "redis", prefix = "user:")
public void updateUser(String userId, User user) {
// Removes cached user when updating
userRepository.save(user);
}
@Invalid(name = "redis", prefix = "user:")
public void deleteUser(String userId) {
// Removes cached user when deleting
userRepository.deleteById(userId);
}
}@Service
public class ProductService {
@Cached(name = "redis", prefix = "product:")
public Product getProduct(
@CacheKey String productId,
@CacheKey("#region.toUpperCase()") String region
) {
// Cache key: product:{productId}{region}
// Example: product:12345US
return productRepository.findByIdAndRegion(productId, region);
}
@Cached(name = "caffeine", prefix = "user:")
public User getUser(
@CacheKey("#userId") String userId,
@CacheKey("#type.name()") UserType type
) {
// Cache key: user:{userId}{type}
// Example: user:12345PREMIUM
return userRepository.findByIdAndType(userId, type);
}
}@Service
public class UserService {
@Cached(name = "redis", prefix = "user:")
public Map<String, User> getUsers(@CacheKey Collection<String> userIds) {
// Generates separate cache key for each user ID
// Example: user:12345, user:67890
return userRepository.findByIds(userIds);
}
@Cached(name = "caffeine", prefix = "product:")
public List<Product> getProductsByIds(@CacheKey List<String> productIds) {
// Batch cache lookup with automatic key generation
return repository.findAllById(productIds);
}
}@Configuration
public class RedisCacheConfig {
@Bean
public JedisPool jedisPool() {
return new JedisPool("localhost", 6379);
}
@Bean
public CacheX<String, Object> redisCache(JedisPool jedisPool) {
// Use Hessian2 serializer for better performance
return new RedisCache<>(jedisPool, new Hessian2Serializer());
}
@Bean
public CacheX<String, Object> redisJsonCache(JedisPool jedisPool) {
// Use JSON serializer for better compatibility
return new RedisCache<>(jedisPool, new FastJsonSerializer());
}
}RedisCache implements AutoCloseable. Call close() (or use try-with-resources) to return all pool connections. The
@PreDestroy annotation ensures automatic shutdown in Spring containers.
@Configuration
public class RedisClusterConfig {
@Bean
public JedisCluster jedisCluster() {
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("localhost", 7000));
nodes.add(new HostAndPort("localhost", 7001));
nodes.add(new HostAndPort("localhost", 7002));
return new JedisCluster(nodes);
}
@Bean
public CacheX<String, Object> redisClusterCache(JedisCluster jedisCluster) {
return new RedisClusterCache<>(jedisCluster, new Hessian2Serializer());
}
}RedisClusterCache also implements AutoCloseable.
Cluster scan limitation: Redis Cluster does not support cross-slot
SCANthrough a single cursor.RedisClusterCache.scan(prefix)performs a best-effort scan reachable from cursor"0". For complete coverage across all shards, route all keys to the same slot using a hash tag (e.g.{ns}:key).
Pass CacheExpire.FOREVER (0) as the expiry argument to persist an entry indefinitely:
// Store configuration that must never expire
cache.write("global:config", config, CacheExpire.FOREVER);All implementations honour this contract:
MemoryCache— entry is never evicted by TTL checksRedisCache/RedisClusterCache— issues a plainSETwithoutPXMemcachedCache— falls back to Memcached's maximum TTL of 30 days (protocol limitation)
Extend the lifetime of an existing entry without rewriting its value:
// Extend session TTL on each user interaction
boolean refreshed = cache.renew(sessionKey, CacheExpire.HALF_HOUR);
if (!refreshed) {
// Key has already expired — recreate the session
}RedisCache and RedisClusterCache implement renew via a single PEXPIRE command.
MemoryCache and other implementations use a read-then-write fallback.
Atomically increment a counter and retrieve the new value:
// Rate limiting: track requests per client per minute
long count = cache.increment("ratelimit:" + clientId + ":" + minute);
if (count > MAX_REQUESTS_PER_MINUTE) {
throw new RateLimitException();
}- Redis: backed by the native
INCRcommand — atomicity guaranteed even under concurrency. - MemoryCache: backed by
AtomicLong— also thread-safe. - Counters start at
1on first call (key created with value0, then incremented). - Counter keys share no TTL by default; call
remove()explicitly to reset.
Retrieve all entries whose key begins with a given prefix:
// Fetch all cached items under a namespace
Map<String, Object> items = cache.scan("product:category:electronics:");Note: For
RedisClusterCache, cross-shard scan is not fully supported. See the cluster scan limitation note above.
@Service
public class CacheMonitorService {
@Autowired
private Collector cacheCollector;
public void printCacheStats() {
Map<String, Snapshot> stats = cacheCollector.getHitting();
stats.forEach((pattern, snapshot) -> {
System.out.println("Cache Pattern: " + pattern);
System.out.println(" Hits: " + snapshot.getHit());
System.out.println(" Requests: " + snapshot.getRequired());
System.out.println(" Hit Rate: " + snapshot.getRate());
});
}
public void resetCacheStats(String pattern) {
cacheCollector.reset(pattern);
}
}@Service
public class LocalCacheService {
private final MemoryCache<String, User> cache = new MemoryCache<>(1000, 180000);
public void printStats() {
// Get built-in statistics
String stats = cache.getStats();
System.out.println(stats);
// Output example:
// MemoryCacheStats[requests=10000, hits=9500, hitRate=95.00%, size=800]
// Get estimated size
long size = cache.estimatedSize();
System.out.println("Current cache size: " + size);
}
}@Service
public class CaffeineCacheService {
private final CaffeineCache<String, Product> cache =
new CaffeineCache<>(10000, 3600000);
public void printStats() {
// Get Caffeine's native statistics
String stats = cache.getStats();
System.out.println(stats);
// Access native cache for advanced operations
com.github.benmanes.caffeine.cache.Cache<String, Product> nativeCache =
cache.getNativeCache();
CacheStats caffeineStats = nativeCache.stats();
System.out.println("Hit Rate: " + caffeineStats.hitRate());
System.out.println("Eviction Count: " + caffeineStats.evictionCount());
}
}@Service
public class SecureUserService {
@Cached(
name = "redis",
prefix = "user:",
expire = 60 * 60 * 1000,
enablePenetrationProtect = true
)
public User getUser(String userId) {
User user = userRepository.findById(userId);
if (user == null) {
// Cache penetration prevention:
// Automatically caches a placeholder object
// to prevent repeated database queries for non-existent users
return null;
}
return user;
}
}@Service
public class ConditionalCacheService {
@Cached(
name = "caffeine",
prefix = "premium_user:",
condition = "#user.type == T(com.example.UserType).PREMIUM"
)
public UserProfile getPremiumProfile(User user) {
// Only cache premium users
return profileRepository.findByUserId(user.getId());
}
@Cached(
name = "redis",
prefix = "product:",
condition = "#price > 0 && #price < 10000"
)
public Product getProductInRange(String productId, BigDecimal price) {
// Only cache products within price range
return productRepository.findById(productId);
}
@Cached(
name = "memory",
prefix = "config:",
condition = "#env == 'production'"
)
public String getProductionConfig(String key, String env) {
// Only cache in production environment
return configRepository.get(key);
}
}@Configuration
public class SerializerConfig {
@Bean
public CacheX<String, Object> customRedisCache(JedisPool jedisPool) {
// Custom serializer for specific serialization needs
BaseSerializer serializer = new BaseSerializer() {
@Override
public byte[] serialize(Object obj) {
// Custom serialization logic
return CustomSerializer.encode(obj);
}
@Override
public Object deserialize(byte[] bytes) {
// Custom deserialization logic
return CustomSerializer.decode(bytes);
}
};
return new RedisCache<>(jedisPool, serializer);
}
}// ✅ Recommended: Use MemoryCache for short-lived, frequently accessed data
CacheX<String, Session> sessionCache = new MemoryCache<>(1000, 180000);
// ✅ Recommended: Use CaffeineCache for high-performance local caching
CacheX<String, Product> productCache = new CaffeineCache<>(10000, 3600000);
// ✅ Recommended: Use RedisCache for distributed caching
CacheX<String, User> userCache = new RedisCache<>(jedisPool, new Hessian2Serializer());
// ❌ Not Recommended: Use distributed cache for local session data
// Unnecessary network overhead// ✅ Recommended: Short expiration for frequently changing data
@Cached(name = "redis", prefix = "stock:", expire = 60 * 1000) // 1 minute
public Stock getStock(String symbol) {
return stockRepository.getRealtime(symbol);
}
// ✅ Recommended: Long expiration for rarely changing data
@Cached(name = "caffeine", prefix = "config:", expire = 24 * 60 * 60 * 1000) // 24 hours
public Config getConfig(String key) {
return configRepository.findByKey(key);
}
// ❌ Not Recommended: No expiration for dynamic data
@Cached(name = "redis", prefix = "price:", expire = CacheExpire.FOREVER)
public Price getPrice(String productId) {
// FOREVER = 0: entry persists until explicitly removed — stale data served indefinitely
}// ✅ Recommended: Batch read for better performance
public Map<String, User> getUsers(List<String> userIds) {
return cache.read(userIds); // Single operation
}
// ❌ Not Recommended: Loop read
public Map<String, User> getUsers(List<String> userIds) {
Map<String, User> result = new HashMap<>();
for (String userId : userIds) {
result.put(userId, cache.read(userId)); // Multiple operations
}
return result;
}// ✅ Recommended: Proper cache-aside implementation
public Product getProduct(String productId) {
Product product = cache.read(productId);
if (product == null) {
product = productRepository.findById(productId);
if (product != null) {
cache.write(productId, product, 3600000);
}
}
return product;
}
// ❌ Not Recommended: No null check causes repeated DB queries
public Product getProduct(String productId) {
Product product = cache.read(productId);
if (product == null) {
// Caches null results - repeated for non-existent products
product = productRepository.findById(productId);
cache.write(productId, product, 3600000);
}
return product;
}// ✅ Recommended: Regular monitoring and alerting
@Scheduled(fixedRate = 60000) // Every minute
public void monitorCache() {
Map<String, Snapshot> stats = cacheCollector.getHitting();
stats.forEach((pattern, snapshot) -> {
double hitRate = Double.parseDouble(snapshot.getRate().replace("%", ""));
if (hitRate < 80.0) {
logger.warn("Low cache hit rate for {}: {}", pattern, snapshot.getRate());
// Send alert or adjust cache configuration
}
});
}// ✅ Recommended: Hierarchical cache keys
@Cached(prefix = "user:profile:")
public UserProfile getUserProfile(String userId) {
// Cache key: user:profile:{userId}
}
@Cached(prefix = "user:settings:")
public UserSettings getUserSettings(String userId) {
// Cache key: user:settings:{userId}
}
// ❌ Not Recommended: Flat namespace
@Cached(prefix = "cache_")
public Object getData(String type, String id) {
// Easy conflicts, hard to invalidate
}All cache implementations that hold external connections (RedisCache, RedisClusterCache,
MemcachedCache) implement AutoCloseable. Release resources when they are no longer needed:
// ✅ Spring beans: @PreDestroy is triggered automatically — no manual close() required
@Bean
public CacheX<String, Object> redisCache(JedisPool jedisPool) {
return new RedisCache<>(jedisPool); // closed automatically on context shutdown
}
// ✅ Non-Spring usage: use try-with-resources
try (RedisCache<String, Object> cache = new RedisCache<>(jedisPool)) {
cache.write("key", value, CacheExpire.ONE_HOUR);
}
// ✅ Prefer renew() over read+write when only the TTL needs extending
boolean extended = cache.renew(sessionKey, CacheExpire.HALF_HOUR);A: Simply change the configuration or bean definition:
// Before: MemoryCache
@Bean
public CacheX<String, Object> cache() {
return new MemoryCache<>(1000, 180000);
}
// After: RedisCache (no code changes needed in service layer)
@Bean
public CacheX<String, Object> cache(JedisPool jedisPool) {
return new RedisCache<>(jedisPool);
}A: Use appropriate serializers:
// For Java Serializable objects
CacheX<String, Object> cache = new RedisCache<>(jedisPool, new JdkSerializer());
// For better performance (Hessian)
CacheX<String, Object> cache = new RedisCache<>(jedisPool, new Hessian2Serializer());
// For JSON compatibility
CacheX<String, Object> cache = new RedisCache<>(jedisPool, new FastJsonSerializer());
// For compression (reduces memory usage)
CacheX<String, Object> cache = new RedisCache<>(jedisPool, new JdkGzipSerializer());A: Use cache lock mechanisms or refresh-ahead:
@Service
public class SafeUserService {
private final Lock lock = new ReentrantLock();
public User getUser(String userId) {
User user = cache.read(userId);
if (user == null) {
lock.lock();
try {
// Double-check locking
user = cache.read(userId);
if (user == null) {
user = userRepository.findById(userId);
cache.write(userId, user, 3600000);
}
} finally {
lock.unlock();
}
}
return user;
}
}A: Use L1 (local) + L2 (distributed) cache pattern:
@Service
public class MultiLevelCacheService {
private final CacheX<String, User> l1Cache = new CaffeineCache<>(1000, 60000);
private final CacheX<String, User> l2Cache = new RedisCache<>(jedisPool);
public User getUser(String userId) {
// Try L1 cache first
User user = l1Cache.read(userId);
if (user == null) {
// Try L2 cache
user = l2Cache.read(userId);
if (user == null) {
// Fetch from database
user = userRepository.findById(userId);
// Populate both caches
l2Cache.write(userId, user, 3600000);
l1Cache.write(userId, user, 60000);
} else {
// Populate L1 from L2
l1Cache.write(userId, user, 60000);
}
}
return user;
}
}A: Use @Invalid annotation with proper key patterns:
@Service
public class CacheInvalidationService {
@Cached(prefix = "user:")
public User getUser(String userId) {
return userRepository.findById(userId);
}
@Cached(prefix = "user:orders:")
public List<Order> getUserOrders(String userId) {
return orderRepository.findByUserId(userId);
}
@Invalid(prefix = "user:")
@Invalid(prefix = "user:orders:")
public void updateUser(String userId, User user) {
// Invalidates both user and user orders caches
userRepository.save(user);
}
}A: Enable debug logging and inspect generated keys:
# application.yml
logging:
level:
org.miaixz.bus.cache: DEBUG// Programmatic key generation
@Component
public class CacheKeyDebugger {
public void debugKeyGeneration(Method method, Object[] args) {
AnnoHolder holder = CacheInfoContainer.getCacheInfo(method).getLeft();
if (holder.isMulti()) {
Map[] maps = Builder.generateMultiKey(holder, args);
System.out.println("Generated keys: " + maps[1].keySet());
} else {
String key = Builder.generateSingleKey(holder, args);
System.out.println("Generated key: " + key);
}
}
}A: Implement cache preloading on startup:
@Component
public class CacheWarmupService implements ApplicationRunner {
@Autowired
private CacheX<String, Product> productCache;
@Autowired
private ProductRepository productRepository;
@Override
public void run(ApplicationArguments args) {
// Preload hot data into cache
List<Product> hotProducts = productRepository.findHotProducts();
Map<String, Product> productMap = hotProducts.stream()
.collect(Collectors.toMap(
Product::getId,
Function.identity()
));
productCache.write(productMap, 3600000);
logger.info("Cache warmup completed: {} products loaded", hotProducts.size());
}
}| Bus Cache Version | JDK Version | Spring Boot Version |
|---|---|---|
| 8.x | 17+ | 3.x+ |
| 7.x | 11+ | 2.x+ |
Based on benchmark tests (10,000 operations):
| Cache Implementation | Avg Latency | Throughput | Hit Rate |
|---|---|---|---|
| MemoryCache | |||
| CaffeineCache | |||
| GuavaCache | |||
| RedisCache | N/A | ||
| MemcachedCache | N/A |
| Property | Type | Default | Description |
|---|---|---|---|
maximumSize |
long |
1000 |
Maximum number of entries |
expireAfterWrite |
long |
180000 |
TTL in milliseconds |
expireAfterAccess |
long |
0 |
TTI in milliseconds (0 = disabled) |
initialCapacity |
int |
16 |
Initial map capacity |
| Property | Type | Default | Description |
|---|---|---|---|
maximumSize |
long |
1000 |
Maximum number of entries |
expireAfterWrite |
long |
- | TTL in milliseconds |
expireAfterAccess |
long |
- | TTI in milliseconds |
initialCapacity |
int |
- | Initial map capacity |
| Property | Type | Default | Description |
|---|---|---|---|
host |
String |
localhost |
Redis server host |
port |
int |
6379 |
Redis server port |
timeout |
int |
2000 |
Connection timeout (ms) |
serializer |
BaseSerializer |
Hessian2Serializer |
Value serializer |
Would you like to search for a tutorial video on YouTube for getting started with Bus Cache?