lex*_*ore 16 java spring caching
在基于Spring的应用程序中,我有一个执行某些计算的服务Index.Index计算(比如1s)相对昂贵,但检查现状(比如20ms)相对便宜.实际代码无关紧要,它遵循以下几行:
public Index getIndex() {
return calculateIndex();
}
public Index calculateIndex() {
// 1 second or more
}
public boolean isIndexActual(Index index) {
// 20ms or less
}
Run Code Online (Sandbox Code Playgroud)
我正在使用Spring Cache通过@Cacheable注释缓存计算的索引:
@Cacheable(cacheNames = CacheConfiguration.INDEX_CACHE_NAME)
public Index getIndex() {
return calculateIndex();
}
Run Code Online (Sandbox Code Playgroud)
我们目前配置GuavaCache为缓存实现:
@Bean
public Cache indexCache() {
return new GuavaCache(INDEX_CACHE_NAME, CacheBuilder.newBuilder()
.expireAfterWrite(indexCacheExpireAfterWriteSeconds, TimeUnit.SECONDS)
.build());
}
@Bean
public CacheManager indexCacheManager(List<Cache> caches) {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(caches);
return cacheManager;
}
Run Code Online (Sandbox Code Playgroud)
我还需要检查缓存的值是否仍然是实际值并刷新它(理想情况下是异步的)如果不是.理想情况下应该如下:
getIndex()被调用时,弹簧会检查是否在高速缓存中的值.
calculateIndex()缓存加载新值并将其存储在缓存中isIndexActual(...).
基本上我想快速提供缓存中的值(即使它已经过时),但也会立即触发刷新.
到目前为止我所做的工作是检查现状和驱逐:
@Cacheable(cacheNames = INDEX_CACHE_NAME)
@CacheEvict(cacheNames = INDEX_CACHE_NAME, condition = "target.isObsolete(#result)")
public Index getIndex() {
return calculateIndex();
}
Run Code Online (Sandbox Code Playgroud)
如果结果已过时,则此检查会触发驱逐,并立即返回旧值,即使是这种情况.但这不会刷新缓存中的值.
是否有办法配置Spring Cache以在驱逐后主动刷新过时的值?
更新
这是一个MCVE.
public static class Index {
private final long timestamp;
public Index(long timestamp) {
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
}
public interface IndexCalculator {
public Index calculateIndex();
public long getCurrentTimestamp();
}
@Service
public static class IndexService {
@Autowired
private IndexCalculator indexCalculator;
@Cacheable(cacheNames = "index")
@CacheEvict(cacheNames = "index", condition = "target.isObsolete(#result)")
public Index getIndex() {
return indexCalculator.calculateIndex();
}
public boolean isObsolete(Index index) {
long indexTimestamp = index.getTimestamp();
long currentTimestamp = indexCalculator.getCurrentTimestamp();
if (index == null || indexTimestamp < currentTimestamp) {
return true;
} else {
return false;
}
}
}
Run Code Online (Sandbox Code Playgroud)
现在测试:
@Test
public void test() {
final Index index100 = new Index(100);
final Index index200 = new Index(200);
when(indexCalculator.calculateIndex()).thenReturn(index100);
when(indexCalculator.getCurrentTimestamp()).thenReturn(100L);
assertThat(indexService.getIndex()).isSameAs(index100);
verify(indexCalculator).calculateIndex();
verify(indexCalculator).getCurrentTimestamp();
when(indexCalculator.getCurrentTimestamp()).thenReturn(200L);
when(indexCalculator.calculateIndex()).thenReturn(index200);
assertThat(indexService.getIndex()).isSameAs(index100);
verify(indexCalculator, times(2)).getCurrentTimestamp();
// I'd like to see indexCalculator.calculateIndex() called after
// indexService.getIndex() returns the old value but it does not happen
// verify(indexCalculator, times(2)).calculateIndex();
assertThat(indexService.getIndex()).isSameAs(index200);
// Instead, indexCalculator.calculateIndex() os called on
// the next call to indexService.getIndex()
// I'd like to have it earlier
verify(indexCalculator, times(2)).calculateIndex();
verify(indexCalculator, times(3)).getCurrentTimestamp();
verifyNoMoreInteractions(indexCalculator);
}
Run Code Online (Sandbox Code Playgroud)
我希望在从缓存中逐出后不久刷新值.目前,它会在下一次调用时刷新getIndex().如果在驱逐后立即刷新了这个值,那么这将节省我1秒钟.
我试过了@CachePut,但它也没有给我带来预期的效果.刷新该值,但无论是什么condition或方法,都始终执行该方法unless.
我目前看到的唯一方法是调用getIndex()两次(第二次异步/非阻塞).但那有点愚蠢.
我想说做你需要的最简单的方法就是创建一个自定义的Aspect,它可以透明地完成所有的魔法,并且可以在更多的地方重复使用.
因此,假设您拥有spring-aop和aspectj依赖于类路径,以下方面将起到作用.
@Aspect
@Component
public class IndexEvictorAspect {
@Autowired
private Cache cache;
@Autowired
private IndexService indexService;
private final ReentrantLock lock = new ReentrantLock();
@AfterReturning(pointcut="hello.IndexService.getIndex()", returning="index")
public void afterGetIndex(Object index) {
if(indexService.isObsolete((Index) index) && lock.tryLock()){
try {
Index newIndex = indexService.calculateIndex();
cache.put(SimpleKey.EMPTY, newIndex);
} finally {
lock.unlock();
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
有几点需要注意
getIndex()方法没有参数,因此它存储在密钥缓存中SimpleKey.EMPTYhello包中.编辑1:
基于@Cacheable和的缓存抽象@CacheEvict在这种情况下将不起作用。这些行为如下:在@Cacheable调用过程中,如果该值在缓存中,则从缓存中返回值,否则计算并放入缓存中,然后返回;在该值从缓存中删除期间@CacheEvict,因此从此时起缓存中没有值,因此第一个传入调用@Cacheable将强制重新计算并放入缓存中。使用@CacheEvict(condition="") 只会根据此条件在调用期间检查是否从缓存值中删除。因此,在每次失效之后,该@Cacheable方法将运行这个重量级例程来填充缓存。
为了将值存储在缓存管理器中并异步更新,我建议重用以下例程:
@Inject
@Qualifier("my-configured-caching")
private Cache cache;
private ReentrantLock lock = new ReentrantLock();
public Index getIndex() {
synchronized (this) {
Index storedCache = cache.get("singleKey_Or_AnythingYouWant", Index.class);
if (storedCache == null ) {
this.lock.lock();
storedCache = indexCalculator.calculateIndex();
this.cache.put("singleKey_Or_AnythingYouWant", storedCache);
this.lock.unlock();
}
}
if (isObsolete(storedCache)) {
if (!lock.isLocked()) {
lock.lock();
this.asyncUpgrade()
}
}
return storedCache;
}
Run Code Online (Sandbox Code Playgroud)
第一个构造是同步的,只是为了阻止所有即将到来的调用,等待第一个调用填充缓存。
然后系统检查是否应该重新生成缓存。如果是,则调用该值的异步更新的单个调用,并且当前线程正在返回缓存的值。一旦缓存处于重新计算状态,即将进行的调用将仅返回缓存中的最新值。等等。
通过这样的解决方案,您将能够重用大量内存,例如 hazelcast 缓存管理器,以及多个基于键的缓存存储,并保留缓存实现和驱逐的复杂逻辑。
或者如果您喜欢这些@Cacheable注释,您可以按以下方式执行此操作:
@Cacheable(cacheNames = "index", sync = true)
public Index getCachedIndex() {
return new Index();
}
@CachePut(cacheNames = "index")
public Index putIntoCache() {
return new Index();
}
public Index getIndex() {
Index latestIndex = getCachedIndex();
if (isObsolete(latestIndex)) {
recalculateCache();
}
return latestIndex;
}
private ReentrantLock lock = new ReentrantLock();
@Async
public void recalculateCache() {
if (!lock.isLocked()) {
lock.lock();
putIntoCache();
lock.unlock();
}
}
Run Code Online (Sandbox Code Playgroud)
与上面几乎相同,但是重用了 spring 的 Caching 注释抽象。
原文:为什么你试图通过缓存来解决这个问题?如果这是简单的值(不是基于键的,您可以以更简单的方式组织代码,请记住 spring 服务默认情况下是单例的)
像这样的东西:
@Service
public static class IndexService {
@Autowired
private IndexCalculator indexCalculator;
private Index storedCache;
private ReentrantLock lock = new ReentrantLock();
public Index getIndex() {
if (storedCache == null ) {
synchronized (this) {
this.lock.lock();
Index result = indexCalculator.calculateIndex();
this.storedCache = result;
this.lock.unlock();
}
}
if (isObsolete()) {
if (!lock.isLocked()) {
lock.lock();
this.asyncUpgrade()
}
}
return storedCache;
}
@Async
public void asyncUpgrade() {
Index result = indexCalculator.calculateIndex();
synchronized (this) {
this.storedCache = result;
}
this.lock.unlock();
}
public boolean isObsolete() {
long currentTimestamp = indexCalculator.getCurrentTimestamp();
if (storedCache == null || storedCache.getTimestamp() < currentTimestamp) {
return true;
} else {
return false;
}
}
}
Run Code Online (Sandbox Code Playgroud)
即第一个调用是同步的,您必须等待结果填充。然后,如果存储的值已过时,系统将执行该值的异步更新,但当前线程将接收存储的“缓存”值。
我还引入了可重入锁来限制存储索引的单次升级。
| 归档时间: |
|
| 查看次数: |
5043 次 |
| 最近记录: |