Search code examples
spring-bootredishazelcastspring-cache

More than 1 caching storage in Spring Boot app


I am facing a strange issue - I have hazelcast and redis in my project. Suddenly all @Cacheable annotations are putting entries only to hazelcast cache, even if the particular cache name is configured via redis cache builder:

    @Bean
    fun redisCacheManagerBuilderCustomizer(): RedisCacheManagerBuilderCustomizer? {
        return RedisCacheManagerBuilderCustomizer { builder: RedisCacheManagerBuilder ->
            builder
                .withCacheConfiguration(
                    MY_CACHE,
                    RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(3))
                )
        }
    }

Using cache:

@Cacheable(cacheNames = [CacheConfig.MY_CACHE])
@Cacheable(value= [CacheConfig.MY_CACHE])

Both does not work and forwards requests to hazelcast only. How to solve this? Using different cacheManager?


Solution

  • Typically, only 1 caching provider is in use to cache data, such as in the service or data access tier of your Spring [Boot] application using Spring's Cache Abstraction and infrastructure components, such as the CacheManager and caching annotations.

    When multiple caching providers (e.g. Hazelcast and Redis) are on the classpath of your Spring Boot application, then it might be necessary to declare which caching provider (e.g. Redis) you want to [solely] use for caching purposes. With this arrangement, Spring Boot allows you to declare your intentions using the spring.cache.type property as explained in the ref doc, here (see first Tip). Valid values of this property are defined by the enumerated values in the CacheType enum.

    However, if you want to cache data using multiple caching providers at once, then you need to explicitly declare your intentions using this approach as well.

    DISCLAIMER: It has been awhile since I have traced through Spring Boot auto-configuration where caching is concerned, and how it specifically handles multiple caching providers on the application classpath, especially when a specific caching provider has not been declared, such as by explicitly declaring the spring.cache.type property. However, and again, this may actually be your intention, to use multiple caching providers in a single @Cacheable (or @CachePut) service or data access operation. If so, continue reading...

    To do so, you typically use 1 of 2 approaches. These approaches are loosely described in the core Spring Framework's ref doc, here.

    1 approach is to declare the cacheNames of the caches from each caching provider along with the CacheManager, like so:

    @Service
    class CustomerService {
    
      @Cacheable(cacheNames = { "cacheOne", "cacheTwo" }, cacheManager="compositeCacheManager")
      public Customer findBy(String name) {
        // ...
      }
    }
    

    In this case, "cacheOne" would be the name of the Cache managed by caching provider one (e.g. Redis), and "cacheTwo" would be the name of the Cache managed by caching provider two (i.e. "Hazelcast").

    DISCLAIMER: You'd have to play around, but it might be possible to simply declare a single Cache name here (e.g. "Customers"), where the caches (or cache data structures in each caching provider) are named the same, and it would still work. I am not certain, but it seems logical this would work as well.

    The key (no pun intended) to this example, however, is the declaration of the CacheManager using the cacheManager attribute of the @Cacheable annotation. As you know, the CacheManager is the Spring SPI infrastructure component used to find and manage Cache objects (caches from the caching providers) used for caching purposes in your Spring managed beans (such as CustomerService).

    I named this CacheManager deliberately, "compositeCacheManager". Spring's Cache Abstraction provides the CompositeCacheManager implementation, which as the name suggests, composes multiple CacheManagers for use in single cache operation.

    Therefore, you could do the following in you Spring [Boot] application configuration:

    @Configuration
    class MyCachingConfiguration {
    
      @Bean
      RedisCacheManager cacheManager() {
        // ...
      }
    
      @Bean
      HazelcastCacheManager hazelcastCacheManager() {
        // ...
      }
    
      @Bean
      CompositeCacheManager compositeCacheManager(RedisCacheManager redis, HazelcastCacheManager hazelcast) {
        return new CompositeCacheManager(redis, hazelcast);
      }
    }
    

    NOTE: Notice the RedisCacheManager is the "default" CacheManager declaration and cache provider (implementation) used when no cache provider is explicitly declared in a caching operation, since the bean name is "cacheManager".

    Alternatively, and perhaps more easily, you can choose to implement the CacheResolver interface instead. The Javadoc is rather self-explanatory. Be aware of the Thread-safety concerns.

    In this case, you would simply declare a CacheResolver implementation in your configuration, like so:

    @Configuration
    class MyCachingConfiguration {
    
      @Bean
      CacheResolver customCacheResolver() {
        // return your custom CacheResolver implementation
      }
    }
    

    Then in your application service components (beans), you would do:

    @Service
    class CustomerService {
    
      @Cacheable(cacheNames = "Customers", cacheResolver="customCacheResolver")
      public Customer findBy(String name) {
        // ...
      }
    }
    

    DISCLAIMER: I have not tested either approach I presented above here, but I feel reasonably confident this should work as expected. It may need some slight modifications, but should generally be the approach(es) you should follow.

    If you have any troubles, please post back in the comments and I will try to follow up.