I've implemented basic set up of Caffeine cache in Spring Boot app. Below you will find configuration and service method annotated with #Cachable. Unfortunately every call for this method generates database query and new input to cache (with same key):
Debug result with cache content
#Cacheable(cacheNames = {"pic"})
public PictureModel loadPictureById(String id) {
var loadedInstance = pictureRepo.findById(id).orElseThrow(() -> new CustomNotFoundException(PictureModel.class));
loadedInstance.setBody(pictureCompressor.decompressBytes(loadedInstance.getBody()));
return loadedInstance;
}
Configuration:
#EnableCaching
#Configuration
public class CaffeineConfig {
#Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("pic");
cacheManager.setCaffeine(caffeineCacheBuilder());
return cacheManager;
}
Caffeine <Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(500)
.expireAfterAccess(10, TimeUnit.MINUTES)
.weakKeys()
.recordStats();
}
}
Database queries after three hits:
Hibernate:
select
picturemod0_.id as id1_10_0_,
picturemod0_.account_id as account_5_10_0_,
picturemod0_.body as body2_10_0_,
picturemod0_.name as name3_10_0_,
picturemod0_.public_platform_id as public_p6_10_0_,
picturemod0_.service_type_id as service_7_10_0_,
picturemod0_.type as type4_10_0_
from
image picturemod0_
where
picturemod0_.id=?
Hibernate:
select
picturemod0_.id as id1_10_0_,
picturemod0_.account_id as account_5_10_0_,
picturemod0_.body as body2_10_0_,
picturemod0_.name as name3_10_0_,
picturemod0_.public_platform_id as public_p6_10_0_,
picturemod0_.service_type_id as service_7_10_0_,
picturemod0_.type as type4_10_0_
from
image picturemod0_
where
picturemod0_.id=?
Hibernate:
select
picturemod0_.id as id1_10_0_,
picturemod0_.account_id as account_5_10_0_,
picturemod0_.body as body2_10_0_,
picturemod0_.name as name3_10_0_,
picturemod0_.public_platform_id as public_p6_10_0_,
picturemod0_.service_type_id as service_7_10_0_,
picturemod0_.type as type4_10_0_
from
image picturemod0_
where
picturemod0_.id=?
I would like to achieve one database query and other hits to be served by cache.
Remove weakKeys() from your CaffeineConfig.
From weakKeys() documentation,
Warning: when this method is used, the resulting cache will use identity (==) comparison to determine equality of keys.
In your config , you are computing keys as Object but weakKeys() is comparing them with ==, so keys are matched as not eqaul and cache miss is happening.
Related
I am trying to create and delete a cache using RedisCacheManager with spring-boot and want to use HSET programmatically but am unable to do it. I am able to do it as a simple SET but not as HSET.
This is the bean that I have created.
#Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() //
.entryTtl(Duration.ofHours(1)) //
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory) //
.cacheDefaults(config) //
.build();
And even made the class where I am making the call as #RedisHash but no luck.
#Service
#Slf4j
#RedisHash(value = "CURRENT_CALLS")
public class CacheCleanupService implements Serializable {
#CacheEvict(value = "CURRENT_CALLS" ,key = "(#cacheKey)")
public void redisCacheNumberCleanup(String cacheKey) {
log.info("Key CLEANUP from the cache: {}", cacheKey);
}
#Cacheable(value = "CURRENT_CALLS", key = "(#cacheKey)")
public String redisCacheNumberStore(String cacheKey) {
log.info("Key Add from the cache: {}", cacheKey);
return cacheKey;
}
}
The o/p I am getting is this when calling these above methods from another #Service class.
127.0.0.1:6379> keys CURRENT_CALLS:*
1) "CURRENT_CALLS::+15109100689:+15134631989"
2) "CURRENT_CALLS::+15109100648:+15134631989"
3) "CURRENT_CALLS::+15109100688:+15134631988"
127.0.0.1:6379> get "CURRENT_CALLS::+15109100648:+15134631989"
"+15109100648:+15134631989"
However, I want the o/p like this
127.0.0.1:6379> keys CURRENT_CALLS
1) "CURRENT_CALLS"
127.0.0.1:6379> hgetall "CURRENT_CALLS"
1) "+15109100648:+15134631989"
2) "1"
3) "+15109100688:+15134631988"
4) "2"
5) "+15109100689:+15134631989"
6) "3"
7) "+17326667726:+17722915819"
8) "4"
How to achieve this through spring-boot annotations.
It seems not likely to play around Redis Hashes data type using the annotation based way.
In the Cache Abstraction section of Spring Framework documentation, all the usages are based on basic cache operation (get key and set key value, if in the context of Redis)
As you mentioned that you are marking your service class with #RedisHash, though,
#RedisHash is used on entities, i.e., the models to be persisted in Redis.
See also
another SO question Spring Boot Caching with Redis and Store value as Redis Hash
tut on developer.redis.com Caching REST Services with Redis
I'm trying to configure Spring CacheManager with Hazelcast. Also, I want to configure Hazelcast's Near Cache so I can retrieve the (already deserialized) instance of my cached object.
Here is my configuration
#Bean
public HazelcastInstance hazelcastConfig() {
val config = new Config().setInstanceName("instance");
val serializationConfig = config.getSerializationConfig();
addCacheConfig(config, "USERS")
serializationConfig.addSerializerConfig(new SerializerConfig()
.setImplementation(getSerializer())
.setTypeClass(User.class)
return Hazelcast.newHazelcastInstance(config);
}
#Bean
public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
return new HazelcastCacheManager(hazelcastInstance);
}
#Bean
public PlatformTransactionManager chainedTransactionManager(PlatformTransactionManager jpaTransactionManager, HazelcastInstance hazelcastInstance) {
return new ChainedTransactionManager(
jpaTransactionManager,
new HazelcastTransactionManager(hazelcastInstance)
);
}
// Configure Near Cache
private void addCacheConfig(Config config, String cacheName) {
val nearCacheConfig = new NearCacheConfig()
.setInMemoryFormat(OBJECT)
.setCacheLocalEntries(true)
.setInvalidateOnChange(false)
.setTimeToLiveSeconds(hazelcastProperties.getTimeToLiveSeconds())
.setEvictionConfig(new EvictionConfig()
.setMaxSizePolicy(ENTRY_COUNT)
.setEvictionPolicy(EvictionPolicy.LRU)
.setSize(hazelcastProperties.getMaxEntriesSize()));
config.getMapConfig(cacheName)
.setInMemoryFormat(BINARY)
.setNearCacheConfig(nearCacheConfig);
}
Saving and retrieving from the Cache is working fine, but my object is deserialized every time I have a cache hit. I want to avoid this deserialization time using a NearCache, but it doesn´t work. I also tried BINARY memory format.
Is this possible with Hazelcast? Or is this deserialization always executed even if I have a NearCache?
Thanks
So after a few changes, it is working now. Here is my conclusion:
So in order to have NearCache working with Spring Cache, all your cached objects should be Immutable. This means final classes and final fields. Also, they all should extend the Serializable interface.
I'm using Caffeine Cache library for Spring Cache. Is there a way to get all the cached keys?
My current application works on a near-realtime data, with the flow as :
In the Cache Updater Thread(which runs at a fixed interval, irrespective of the user request), I need to get all the keys currently in the Cache, fetch their latest data from Db & then use #CachePut to update the cache.
Yo can inject CacheManager and obtain native cache from it.
#AllArgsConstructor
class Test {
private CacheManager cacheManager;
Set<Object> keys(String cacheName){
CaffeineCache caffeineCache = (CaffeineCache) cacheManager.getCache(cacheName);
com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache = caffeineCache.getNativeCache();
return nativeCache.asMap().keySet();
}
}
Of course you should add some class casting checks.
You can return keyset by using asMap().keySet() method as follows.
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
class Test{
private Cache<String,String> testCache;
Test(){
testCache = Caffeine.newBuilder().expireAfterWrite( 3000, TimeUnit.SECONDS).build();
}
// return keys as a set
public Set<String> getCacheKeySet(){
return testCache.asMap().keySet();
}
I'm using spring boot with second level cache for the entities, e.g.
#Entity
#Table(name = "customer")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Customer implements Serializable {
....
}
This is working as expected, but now I have to turn the application into a multi-tenant version. Each tenant has it's own database, where we use a ThreadLocal to store the current tenant and AbstractRoutingDataSource for routing to the tenant's database. This is working, if the 2nd level cache is off.
It would be nice to get also the 2nd level cache working. The problem seems to be the cache name, which is the FQCN for an entity. Since the cache is not tenant or database-aware, each tenant uses the same cache.
For this, we use a ThreadLocal for resolving the current tenant is simply accessible by
TenantContext.getCurrentTenant();
and returns the tenant name.
We use the EhCache, which is backed by the spring cache abstraction:
#Bean
public CacheManager cacheManager() {
net.sf.ehcache.CacheManager cacheManager = net.sf.ehcache.CacheManager.create();
EhCacheCacheManager ehCacheManager = new EhCacheCacheManager();
ehCacheManager.setCacheManager(cacheManager);
return ehCacheManager;
}
Is it possible to intercept the generation of the cache-name, so that the current tenant name is used instead of the FQCN and each insert/lookup/evict resolves this tenant-aware-cache-name?
In order to intercept the generation of the cache-name one approach is to override the method getCache(String name) of the EhCacheCacheManager as follows:
#Bean
public CacheManager cacheManager() {
net.sf.ehcache.CacheManager cacheManager = net.sf.ehcache.CacheManager.create();
EhCacheCacheManager ehCacheManager = new EhCacheCacheManager(){
#Override
public Cache getCache(String name) {
String tenantId = TenantContext.getCurrentTenant();
return super.getCache(String.format("%s:%s", tenantId, name));
}
};
ehCacheManager.setCacheManager(cacheManager);
return ehCacheManager;
}
I have a problem when trying to get back a Guava cache from a cache manager, instead I get a Spring Cache.
This is the bean in my SpringConfig file :
#Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager() {
#Override
protected Cache createConcurrentMapCache(final String name) {
return new ConcurrentMapCache(name, CacheBuilder.newBuilder().expireAfterWrite(1440, TimeUnit.MINUTES)
.maximumSize(100).recordStats().build().asMap(), false); }
};
return cacheManager;
}
And then I'm able to use the #Cacheable :
#Cacheable(value = "myCache")
public void myCacheMethod(String key){
// call a web service
}
Everything works fine, but I can't get the cache Guava object created by the CacheBuilder in order to call the stats() method.
This is how I get the cache :
Cache myCache = cacheManager.getCache("myCache");
ValueWrapper wrapper = myCache.get("key");
WebServiceType myCachedObject= (WebServiceType) wrapper.get();
The last cache is a Spring cache, and I get an error if I cast it to Guava cache.
Is this possible ? Or did I do something wrong ?