javaspring-bootspring-data-redisspring-cache

Unable to update already cached list using @CachePut


I am using redis cache in spring boot application. Requirement is - We need data list on certain date filter. So to achieve that I used cacheable annotation.

Now when I run it for first time, it fetches records from db. When run multiple times it returns data stored in cache. Perfect! till here it is running fine.

Then I make an entry in db using my jpa repository, with CachePut annotation.

Now, assuming that I will again get the cache data with updated entry, I make a request to my GET api which I tried above. But to my surprise it fails to execute. Forget about the latest entry, this time it didn't even show earlier cached data list also.

    @Cacheable(cacheNames = "custDetails", key="#newApplicationDate")
    @Override
    public List<FetchResponse> fetchAll2(String newApplicationDate) {
        //some code to return list
    }


@CachePut(cacheNames = "custDetails", key="#strDate")
@Override
public CustomerDetail updateCustomer(CustomerDetail customerDetail,String strDate) {
    CustomerDetail customerDetail2 =  custRepository.saveAndFlush(customerDetail);
    return customerDetail2;
}

I crossed check using print statements, both newApplicationDate and strDate date values are same.

In redis client I checked keys * it showed something like:

1) "custDetails:\xac\xed\x00\x05t\x00\n2023-11-01"

and in spring boot console I am getting following error:

java.lang.ClassCastException: com.myapp.model.CustomerDetail cannot be cast to java.util.List
    at com.myapp.service.MyServiceImpl$$EnhancerBySpringCGLIB$$347be321.fetchAll2(<generated>) ~[classes/:na]

How can I update the existing cached list using cachePut annotation?

=========================================

I also tried below code i.e. calling a same return type method again, this time I put the cachePut annotation now in this new method:

@Override
public CustomerDetail updateCustomer(CustomerDetail customerDetail,String strDate) {
    // TODO Auto-generated method stub
    System.out.println("!!!!!  "+strDate+"   !!!!!");
    CustomerDetail customerDetail2 =  custRepository.saveAndFlush(customerDetail);
    if(customerDetail2!=null) {
        refreshCache(strDate);
    }
    return customerDetail2;
}
@CachePut(cacheNames = "custDetails", key="#strDate")   
public List<FetchResponse> refreshCache(String strDate){
    return ekycService.fetchAll2(strDate);
}

But no success..

=================UPDATE================

Now I have put both Cacheable and CachePut in same service class.

@Cacheable(value = "custDetails", key = "#date")
    @Override
    public List<CustomerDetail> fetchByDate(String date) {

        return custRepository.findByCreateDatess(date);
        
    }

@CachePut(value = "custDetails", key="#strDate")    
    @Override
    public CustomerDetail updateCustomer(CustomerDetail customerDetail,String strDate) {
        CustomerDetail customerDetail2 =  custRepository.saveAndFlush(customerDetail);
        return customerDetail2;
    }

Again same issue. When I try adding CustomerDetail to the List<CustomerDetail> , then onwards I am not getting List. I am getting below error instead when trying to get List<CustomerDetail>:

java.lang.ClassCastException: com.myapp.model.CustomerDetail cannot be cast to java.util.List

If caching can not sync with its own existing list, then why on earth it is so talked about concept!


Solution

  • As @WildDev explained in his comment above, the return types of the value to cache must match for a given key when using a specifically "named" cache.

    In your examples, this would equate to:

    @Service
    class CacheableCustomerServiceExample {
    
        @Cacheable(cacheNames="customerDetails")
        List<CustomerDetail> fetchByDate(String date) {
            // ...
        }
    
        @CachePut(cacheNames="customerDetails", key="#date")
        List<CustomerDetail> updateCustomer(String date, CustomerDetail customer) {
            // ...
        }
    
    }
    

    Remember, your "customerDetails" cache will have the following entry after the fetchByDate(..) service method is called:

    KEY  | VALUE
    ------------
    date | List<CustomerDetail>
    

    This means you need logic in your updateCustomer(..) service method to update the List of CustomerDetail objects returned by the update method to subsequently update the cache entry for the "date" key.

    WARNING: Keep in mind, you cannot simply call the fetchByDate(..) service method from inside the service proxy, as this won't invoke Spring's caching AOP logic, for reasons explained in the documentation.

    Arguably, if any CustomerDetail entry in the List is updated then the cache entry should be invalidated. Therefore, your updateCustomer(..) method would rather be:

    @CacheEvict(cacheNames="customerDetails", key="#date")
    CustomerDetail updateCustomer(String date, CustomerDetail customer) {
        // ...
    }
    

    In this case, the service method return types don't need to match.

    Yes, this will remove the cached List entirely, but the next time the particular date is requested on fetch (i.e. fetchByDate(..)), then the List can be lazily reloaded/cached.

    This approach might come at the expense of more latency, but consume less memory.

    Also, you need to consider how frequently individual CustomerDetail objects stored in the cache List are updated, in addition to the "concurrency" of the updates. You may possibly run into race conditions with your current arrangement, without some form of synchronization (for example), especially, if your List of CustomerDetail objects are large in size and the concurrency and frequency of updates are high. This in itself defeats the purpose of caching.

    Other things to consider is if some other application is updating the backend data source outside of this application using caching. There are many things to think about, technically.

    TIP: You typically only want to cache non-highly transactional data (non-frequently changing data) that is computationally expense to create or expensive to retrieve (e.g. network bound data, such as calling some Web service, like a REST API), but referred to often.

    Personally, I'd prefer to cache individual objects and not collections of those objects. However, if you are caching a collection, then I'd argue to 1) keep the List of things related and 2) small.

    There are several ways to handle what you want to do in your case, but I present 1 way here.

    WARNING: This approach is not necessarily the best way, and it depends on the use case, requirements and SLAs for your application.

    See the example test I wrote for this post.