Support for multi-threaded cache access
Previously, if a `@Cacheable` method was accessed with the same key by multiple threads, the underlying method was invoked several times instead of blocking the threads while the value is computed. This scenario typically affects users that enable caching to avoid calling a costly method too often. When said method can be invoked by an arbitrary number of clients on startup, caching has close to no effect. This commit adds a new method on `Cache` that implements the read-through pattern: ``` <T> T get(Object key, Callable<T> valueLoader); ``` If an entry for a given key is not found, the specified `Callable` is invoked to "load" the value and cache it before returning it to the caller. Because the entire operation is managed by the underlying cache provider, it is much more easier to guarantee that the loader (e.g. the annotated method) will be called only once in case of concurrent access. A new `sync` attribute to the `@Cacheable` annotation has been addded. When this flag is enabled, the caching abstraction invokes the new `Cache` method define above. This new mode bring a set of limitations: * It can't be combined with other cache operations * Only one `@Cacheable` operation can be specified * Only one cache is allowed * `condition` and `unless` attribute are not supported The rationale behind those limitations is that the underlying Cache is taking care of the actual caching operation so we can't really apply any SpEL or multiple caches handling there. Issue: SPR-9254
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cache.caffeine;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
@@ -88,6 +89,12 @@ public class CaffeineCache extends AbstractValueAdaptingCache {
|
||||
return super.get(key);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T get(Object key, final Callable<T> valueLoader) {
|
||||
return (T) fromStoreValue(this.cache.get(key, new LoadFunction(valueLoader)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object lookup(Object key) {
|
||||
return this.cache.getIfPresent(key);
|
||||
@@ -133,4 +140,23 @@ public class CaffeineCache extends AbstractValueAdaptingCache {
|
||||
}
|
||||
}
|
||||
|
||||
private class LoadFunction implements Function<Object, Object> {
|
||||
|
||||
private final Callable<?> valueLoader;
|
||||
|
||||
public LoadFunction(Callable<?> valueLoader) {
|
||||
this.valueLoader = valueLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object apply(Object o) {
|
||||
try {
|
||||
return toStoreValue(valueLoader.call());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new ValueRetrievalException(o, valueLoader, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cache.ehcache;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import net.sf.ehcache.Ehcache;
|
||||
import net.sf.ehcache.Element;
|
||||
import net.sf.ehcache.Status;
|
||||
@@ -62,10 +64,47 @@ public class EhCacheCache implements Cache {
|
||||
|
||||
@Override
|
||||
public ValueWrapper get(Object key) {
|
||||
Element element = this.cache.get(key);
|
||||
Element element = lookup(key);
|
||||
return toValueWrapper(element);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T get(Object key, Callable<T> valueLoader) {
|
||||
Element element = lookup(key);
|
||||
if (element != null) {
|
||||
return (T) element.getObjectValue();
|
||||
}
|
||||
else {
|
||||
this.cache.acquireWriteLockOnKey(key);
|
||||
try {
|
||||
element = lookup(key); // One more attempt with the write lock
|
||||
if (element != null) {
|
||||
return (T) element.getObjectValue();
|
||||
}
|
||||
else {
|
||||
return loadValue(key, valueLoader);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.cache.releaseWriteLockOnKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private <T> T loadValue(Object key, Callable<T> valueLoader) {
|
||||
T value;
|
||||
try {
|
||||
value = valueLoader.call();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new ValueRetrievalException(key, valueLoader, ex);
|
||||
}
|
||||
put(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T get(Object key, Class<T> type) {
|
||||
@@ -98,6 +137,11 @@ public class EhCacheCache implements Cache {
|
||||
this.cache.removeAll();
|
||||
}
|
||||
|
||||
|
||||
private Element lookup(Object key) {
|
||||
return this.cache.get(key);
|
||||
}
|
||||
|
||||
private ValueWrapper toValueWrapper(Element element) {
|
||||
return (element != null ? new SimpleValueWrapper(element.getObjectValue()) : null);
|
||||
}
|
||||
|
||||
@@ -93,6 +93,25 @@ public class GuavaCache extends AbstractValueAdaptingCache {
|
||||
return super.get(key);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T get(Object key, final Callable<T> valueLoader) {
|
||||
try {
|
||||
return (T) fromStoreValue(this.cache.get(key, new Callable<Object>() {
|
||||
@Override
|
||||
public Object call() throws Exception {
|
||||
return toStoreValue(valueLoader.call());
|
||||
}
|
||||
}));
|
||||
}
|
||||
catch (ExecutionException ex) {
|
||||
throw new ValueRetrievalException(key, valueLoader, ex.getCause());
|
||||
}
|
||||
catch (UncheckedExecutionException ex) {
|
||||
throw new ValueRetrievalException(key, valueLoader, ex.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object lookup(Object key) {
|
||||
return this.cache.getIfPresent(key);
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
package org.springframework.cache.jcache;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import javax.cache.processor.EntryProcessor;
|
||||
import javax.cache.processor.EntryProcessorException;
|
||||
import javax.cache.processor.MutableEntry;
|
||||
|
||||
import org.springframework.cache.support.AbstractValueAdaptingCache;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -69,6 +74,16 @@ public class JCacheCache extends AbstractValueAdaptingCache {
|
||||
return this.cache.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T get(Object key, Callable<T> valueLoader) {
|
||||
try {
|
||||
return this.cache.invoke(key, new ValueLoaderEntryProcessor<T>(), valueLoader);
|
||||
}
|
||||
catch (EntryProcessorException ex) {
|
||||
throw new ValueRetrievalException(key, valueLoader, ex.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(Object key, Object value) {
|
||||
this.cache.put(key, toStoreValue(value));
|
||||
@@ -90,4 +105,30 @@ public class JCacheCache extends AbstractValueAdaptingCache {
|
||||
this.cache.removeAll();
|
||||
}
|
||||
|
||||
|
||||
private class ValueLoaderEntryProcessor<T> implements EntryProcessor<Object, Object, T> {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public T process(MutableEntry<Object, Object> entry, Object... arguments)
|
||||
throws EntryProcessorException {
|
||||
Callable<T> valueLoader = (Callable<T>) arguments[0];
|
||||
if (entry.exists()) {
|
||||
return (T) fromStoreValue(entry.getValue());
|
||||
}
|
||||
else {
|
||||
T value;
|
||||
try {
|
||||
value = valueLoader.call();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new EntryProcessorException("Value loader '" + valueLoader + "' failed " +
|
||||
"to compute value for key '" + entry.getKey() + "'", ex);
|
||||
}
|
||||
entry.setValue(toStoreValue(value));
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cache.transaction;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
@@ -72,6 +74,11 @@ public class TransactionAwareCacheDecorator implements Cache {
|
||||
return this.targetCache.get(key, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T get(Object key, Callable<T> valueLoader) {
|
||||
return this.targetCache.get(key, valueLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(final Object key, final Object value) {
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
|
||||
Reference in New Issue
Block a user