简体   繁体   中英

Spring cache value not being renewed when unless condition is not met

I'm having an issue with the unless condition of the Cacheable annotation.

From the documentation, I understand that the unless condition is verified after the method being annotated is called and the value the method returns is cached (and actually returned) only if the unless condition is not met. Otherwise, the cached value should be returned.

Firstly, is this assumption true?

EDIT:

[From the Spring documentation] As the name implies, @Cacheable is used to demarcate methods that are cacheable - that is, methods for whom the result is stored into the cache so on subsequent invocations (with the same arguments), the value in the cache is returned without having to actually execute the method.

[My understanding] So for a given key, the method will be always executed until the unless condition is not met once. Then the cached value will be returned for all subsequent calls to the method.

To illustrate my issue, I tried to break down my code into four simples classes:

1) DummyObject that represents instances to be cached and retrieved. It's a timestamp wrapper to show what is last value that has been cached. The toBeCached boolean is a flag that should be checked in the unless condition to know if the instance returned should be cached or not.

2) DummyDAO that returns DummyObject instances based on provided keys. Upon retrieval of an instance, the DAO checks when was the last value retrieved and verified if it should be cached or not (independently of what key is provided. Doesn't matter if this logic is "broken" as I'm always using the same key for my example). The DAO then marks the instance returned with the flag toBeCached. If the value is marked to be cached, the DAO actually updates its lastRetrieved timestamp as the instance should be eventually cached by the CachedDAO (because the unless condition won't be met).

3) DummyCachedDao that calls the DummyDAO to retrieve instances of DummyObject. If instances are marked toBeCached, it should cache the newly returned value. It should return the previously cached value otherwise.

4) The Application that retrieves a value (that will be cached), sleeps for a short time (not long enough for the cache duration to pass), retrieves a value (that should be the cached one), sleeps again (long enough for cache duration to pass), retrieves again a value (that should be a new value to be cached).

Unfortunately, this code does not work as expected as the retrieved value is always the original value that has been cached. To ensure that the logic worked as expected, I checked if the unless conditions are met or not by replacing the retrieveTimestamp by retrieveTimestampBypass in the Application class. Since internal calls bypass the Spring proxy, the retrieveTimestamp method and whatever breakpoints or logs I put in are actually caught/shown.

What would cause the value to never be cached again? Does the cache need to be clean from previous values first?

public class DummyObject
{
  private long timestamp;
  private boolean toBeCached;

  public DummyObject(long timestamp, boolean toBeCached)
  {
    this.timestamp = timestamp;
    this.toBeCached = toBeCached;
  }

  public long getTimestamp()
  {
    return timestamp;
  }

  public boolean isToBeCached()
  {
    return toBeCached;
  }
}
@Service
public class DummyDAO
{
  private long cacheDuration = 3000;
  private long lastRetrieved;

  public DummyObject retrieveTimestamp(String key)
  {
    long renewalTime = lastRetrieved + cacheDuration;
    long time = System.currentTimeMillis();
    boolean markedToBeCached = renewalTime < time;

    System.out.println(renewalTime + " < " + time + " = " + markedToBeCached);

    if(markedToBeCached)
    {
      lastRetrieved = time;
    }

    return new DummyObject(time, markedToBeCached);
  }
}
@Service
public class DummyCachedDAO
{
    @Autowired
    private DummyDAO dao;

    // to check the flow.
    public DummyObject retrieveTimestampBypass(String key)
    {
        return retrieveTimestamp(key);
    }

    @Cacheable(cacheNames = "timestamps", unless = "#result.isToBeCached() != true")
    public DummyObject retrieveTimestamp(String key)
    {
      return dao.retrieveTimestamp(key); 
    }
}
@SpringBootApplication
@EnableCaching
public class Application
{
  public final static String KEY = "cache";
  public final static String MESSAGE = "Cached timestamp is: %s [%s]";

  public static void main(String[] args) throws InterruptedException
  {
    SpringApplication app = new SpringApplication(Application.class);
    ApplicationContext context = app.run(args);
    DummyCachedDAO cache = (DummyCachedDAO) context.getBean(DummyCachedDAO.class);

    // new value
    long value = cache.retrieveTimestamp(KEY).getTimestamp();
    System.out.println(String.format(MESSAGE, value, new Date(value)));

    Thread.sleep(1000);

    // expecting same value
    value = cache.retrieveTimestamp(KEY).getTimestamp();
    System.out.println(String.format(MESSAGE, value, new Date(value));

    Thread.sleep(5000);

    // expecting new value
    value = cache.retrieveTimestamp(KEY).getTimestamp();
    System.out.println(String.format(MESSAGE, value, new Date(value));

    SpringApplication.exit(context, () -> 0);
  }
}

There are so many details and maybe issues here but first of all you should remove

private long lastRetrieved;

from DummyDao class. DummyDao is a singleton instance lastRetrieved field is not thread safe.

As you can also see from the logs after you cache the item first time it will always be retrieved from there as it has cached in the first call.

Otherwise you should have seen following log

3000 < 1590064933733 = true

The problem is actually quite simple. There is no solution to my problem and rightfully so.

The original assumption I had was that " the unless condition is verified every time after the method being annotated is called and the value the method returns is cached (and actually returned) only if the unless condition is not met. Otherwise, the cached value should be returned. "

However, this was not the actual behavior because as the documentation states, "@Cacheable is used to demarcate methods that are cacheable - that is, methods for whom the result is stored into the cache so on subsequent invocations (with the same arguments), the value in the cache is returned without having to actually execute the method."

So for a given key, the method will be always executed until the unless condition is not met once. Then the cached value will be returned for all subsequent calls to the method.

So I tried to approach the problem in a different way for my experiment, by trying to use a combination of annotations (@Caching with @Cacheable and @CachePut, although the documentation advises against it). The value that I was retrieving was always the new one while the one in the cache was always the expected one. (*)


That's when I tilted THAT I couldn't upload the value in the cache based on an internal timestamp that would have been generated in the method that is being cached AND retrieving at the same the cached value if the unless condition was met or the new one otherwise.

What would be the point of executing the method every single time to compute the latest value but returning the cached one (because of the unless condition I was setting)? There is no point...

What I wanted to achieve (update the cache if a period expired) would have been possible if the condition of cacheable was to specify when to retrieve the cached version or retrieve/generate a new one. As far as I am aware, the cacheable is only to specify when a method needs to be cached in the first place.


That is the end of my experiment. The need to test this arose when I came across an issue with an actual production project that used an internal timestamp with this unless condition. FYI, the most obvious solution to this problem is to use a cache provider that actually provides TTL capabilities.

(*) PS: I also tried few @caching combinations of @CacheEvict (with condition="#root.target.myNewExpiringCheckMethod()==true") and @Cacheable but it failed as well as the CacheEvict enforce the execution of the annotated method.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM