java - वसंत कैश ताज़ा अप्रचलित मूल्य




spring caching (4)

EDIT1:

@Cacheable और @CacheEvict पर आधारित कैशिंग अमूर्त इस मामले में काम नहीं करेगा। उन व्यवहारों का पालन करना है: @Cacheable कॉल के दौरान यदि मान कैश में है - कैश से लौटा मूल्य, अन्यथा गणना और कैश में डालें और फिर लौटें; @CacheEvict के दौरान मूल्य को कैश से हटा दिया जाता है, इसलिए इस क्षण से कैश में कोई मूल्य नहीं है, और इस तरह से @Cacheable पर आने वाली पहली @Cacheable पुनर्गणना को मजबूर करेगी और कैश में @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;
}

पहला निर्माण sycnrronized है, बस सभी आगामी कॉल ब्लॉक करने के लिए प्रतीक्षा करने के लिए जब तक पहली कॉल कैश की भर्ती

तो सिस्टम मौसम की जांच करता है कैश को पुनर्जीवित किया जाना चाहिए। यदि हां, मान के अतुल्यकालिक अद्यतन के लिए एक कॉल बुलाया जाता है, और वर्तमान थ्रेड कैश्ड वैल्यू वापस कर रहा है। कैश के बाद आने वाली कॉल पुन: पुनर्व्यवस्था की स्थिति में है, केवल कैश से सबसे हाल ही में मूल्य लौटाएगा। और इसी तरह।

इस तरह के हल के साथ आप मेमरी की विशाल मात्रा का पुन: उपयोग कर सकेंगे, हजेलकास्ट कैश मैनेजर के साथ-साथ कई चाबी-आधारित कैश स्टोरेज भी कह सकते हैं और कैश के वास्तविक तर्क और निष्कासन के अपने जटिल तर्क को रख सकते हैं।

या यदि आप @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();
    }
}

जो लगभग ऊपर है, जैसा कि ऊपर है, लेकिन वसंत का कैशिंग एनोटेशन अवशेष पुनः उपयोग करता है।

मूल: आप इसे कैशिंग के माध्यम से हल करने का प्रयास क्यों कर रहे हैं? यदि यह सरल मूल्य है (कुंजी आधारित नहीं है, तो आप अपने कोड को सरल तरीके से व्यवस्थित कर सकते हैं, यह ध्यान में रखते हुए कि वसंत सेवा डिफ़ॉल्ट रूप से सिंगलटन है)

ऐसा कुछ:

@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;
        }
    }
}

यानी पहली कॉल सिंक्रनाइज़ है और आपको तब तक इंतजार करना पड़ता है जब तक परिणाम पॉपुलेटेड नहीं होते हैं। तब अगर संग्रहीत मूल्य अप्रचलित है, तो सिस्टम मूल्य के एसिंक्रोनस अपडेट का प्रदर्शन करेगा, लेकिन वर्तमान थ्रेड संग्रहित "कैश्ड" मान प्राप्त करेगा।

मैंने समय पर संग्रहित सूचकांक के एकल उन्नयन को प्रतिबंधित करने के लिए रीएन्ट्रंट लॉक भी पेश किया था।

वसंत-आधारित अनुप्रयोग में मेरे पास एक सेवा है जो कुछ Index की गणना करता है। वास्तविकता की जांच करने के लिए Index अपेक्षाकृत महंगा है (कहना है, 1 एस) पर अपेक्षाकृत सस्ता है (कहो, 20ms)। वास्तविक कोड कोई फर्क नहीं पड़ता, यह निम्न पंक्तियों के साथ चला जाता है:

public Index getIndex() {
    return calculateIndex();
}

public Index calculateIndex() {
    // 1 second or more
}

public boolean isIndexActual(Index index) {
    // 20ms or less
}

मैं @Cacheable एनोटेशन के माध्यम से गणना सूचक को कैश करने के लिए स्प्रिंग कैश का उपयोग कर रहा हूं:

@Cacheable(cacheNames = CacheConfiguration.INDEX_CACHE_NAME)
public Index getIndex() {
    return calculateIndex();
}

हम वर्तमान में GuavaCache कार्यान्वयन के रूप में GuavaCache कैश कॉन्फ़िगर 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;
}

मुझे इसकी भी जांच करने की आवश्यकता है कि क्या कैश्ड वैल्यू अभी भी वास्तविक है और इसे ताज़ा करे (आदर्श रूप में एसिंक्रोनस) यदि यह नहीं है। इसलिए आदर्श रूप में निम्नानुसार जाना चाहिए:

  • जब getIndex() कहा जाता है, स्प्रिंग की जांच करता है कि क्या कैश में कोई मान है।
    • यदि नहीं, तो नया मान calculateIndex() द्वारा लोड किया जाता है और कैश में संग्रहीत होता है
    • यदि हां, तो मौजूदा मान को isIndexActual(...) माध्यम से वास्तविकता के लिए चेक किया जाता है।
      • अगर पुराने मूल्य वास्तविक है, तो इसे वापस कर दिया जाता है।
      • यदि पुराने मूल्य वास्तविक नहीं है, तो इसे वापस कर दिया जाता है, लेकिन कैश से हटा दिया जाता है और नए मान के लोड होने से भी अच्छी शुरुआत होती है

असल में मैं कैश से बहुत तेजी से मूल्य की सेवा करना चाहता हूं (भले ही यह अप्रचलित हो) लेकिन अभी भी ताज़ा होने के कारण

मैं अब तक काम कर रहा हूं, वास्तविकता और निष्कासन की जांच कर रहा हूं:

@Cacheable(cacheNames = INDEX_CACHE_NAME)
@CacheEvict(cacheNames = INDEX_CACHE_NAME, condition = "target.isObsolete(#result)")
public Index getIndex() {
    return calculateIndex();
}

यह चेक निष्कासन को ट्रिगर करता है, यदि परिणाम अप्रचलित है और पुराने मूल्य को तत्काल भले ही मामला मिल जाए। लेकिन यह कैश में मान रिफ्रेश नहीं करता है।

निष्कासन के बाद अप्रचलित मूल्यों को सक्रिय रूप से ताज़ा करने के लिए स्प्रिंग कैश को कॉन्फ़िगर करने का कोई तरीका है?

अद्यतन करें

यहाँ एक 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;
        }
    }
}

अब परीक्षण:

@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);
}

कैश से बेदखल होने के कुछ समय बाद मुझे रिफ्रेश किया गया होना चाहूंगा। फिलहाल यह getIndex() की अगली कॉल पर ताज़ा हो जाता है। यदि मूल्य निष्कासन के बाद ठीक रीफ्रेश होता, तो यह मुझे बाद में 1 से बचाएगा।

मैंने @CachePut की कोशिश की है, लेकिन यह भी मुझे वांछित प्रभाव नहीं मिल रहा है मान ताज़ा हो जाता है, लेकिन विधि हमेशा निष्पादित होती है, चाहे कोई भी condition unless हो या unless

इस समय मैं देख रहा हूं कि एकमात्र तरीका है getIndex() दो बार (दूसरी बार getIndex() / गैर-अवरुद्ध) कॉल getIndex() । लेकिन यह बेवकूफी की तरह है


मैं कहूंगा कि आपको क्या करने का सबसे आसान तरीका एक कस्टम पहलू बनाना है जो सभी जादू को पारदर्शी तरीके से करवाएगा और इसे और अधिक स्थानों में पुन: उपयोग किया जा सकता है।

तो मान लें कि आपके पास अपने वर्ग पथ पर 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();
            }
        }
    }
}

नोट करने के लिए कई चीजें

  1. जैसा कि आपकी getIndex() पद्धति में कोई मानदंड नहीं है, वह SimpleKey.EMPTY लिए कैश में संग्रहीत है SimpleKey.EMPTY
  2. कोड मानता है कि IndexService hello पैकेज में है।

मैं आपकी इंडेक्स सर्विस में गुवा लोडिंग कैश का उपयोग करेगा, जैसे नीचे दिए गए कोड नमूने में दिखाया गया है:

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
 .maximumSize(1000)
 .refreshAfterWrite(1, TimeUnit.MINUTES)
 .build(
     new CacheLoader<Key, Graph>() {
       public Graph load(Key key) { // no checked exception
         return getGraphFromDatabase(key);
       }
       public ListenableFuture<Graph> reload(final Key key, Graph prevGraph) {
         if (neverNeedsRefresh(key)) {
           return Futures.immediateFuture(prevGraph);
         } else {
           // asynchronous!
           ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() {
             public Graph call() {
               return getGraphFromDatabase(key);
             }
           });
           executor.execute(task);
           return task;
         }
       }
     });

आप गवावा की विधि को कॉल करके एक async रीलोडिंग कैश लोडर बना सकते हैं:

public abstract class CacheLoader<K, V> {
...

  public static <K, V> CacheLoader<K, V> asyncReloading(
      final CacheLoader<K, V> loader, final Executor executor) {
      ...
      
  }
}

उदाहरण के लिए थ्रेडपूल एक्साक्षक द्वारा एक अलग थ्रेड में रीलोड कार्रवाई को चलाने के लिए चाल है:

  • पहली कॉल पर, कैश भार () पद्धति से आबादी है, इस प्रकार यह जवाब देने में कुछ समय ले सकता है,
  • बाद के कॉलों पर, जब मूल्य को ताज़ा करना पड़ता है, यह अभी भी बासी मूल्य की सेवा करते समय अतुल्यकालिक गणना की जा रही है। रीफ्रेश पूर्ण होने के बाद यह अपडेटेड मान की सेवा देगा।

मुझे लगता है कि ऐसा कुछ हो सकता है

@Autowired
IndexService indexService; // self injection

@Cacheable(cacheNames = INDEX_CACHE_NAME)
@CacheEvict(cacheNames = INDEX_CACHE_NAME, condition = "target.isObsolete(#result) && @indexService.calculateIndexAsync()")
public Index getIndex() {
    return calculateIndex();
}

public boolean calculateIndexAsync() {
    someAsyncService.run(new Runable() {
        public void run() {
            indexService.updateIndex(); // require self reference to use Spring caching proxy
        }
    });
    return true;
}

@CachePut(cacheNames = INDEX_CACHE_NAME)
public Index updateIndex() {
    return calculateIndex();
}

उपरोक्त कोड में कोई समस्या है, यदि आप अपडेट होने के समय फिर से प्राप्त getIndex() को कॉल करते हैं, तो इसकी गणना फिर से की जाएगी। इसे रोकने के लिए, @CacheEvict उपयोग न करें और सूचकांक द्वारा गणना किए जाने तक @Cacheable अप्रचलित मान वापस दें।

@Autowired
IndexService indexService; // self injection

@Cacheable(cacheNames = INDEX_CACHE_NAME, condition = "!(target.isObsolete(#result) && @indexService.calculateIndexAsync())")
public Index getIndex() {
    return calculateIndex();
}

public boolean calculateIndexAsync() {
    if (!someThreadSafeService.isIndexBeingUpdated()) {
        someAsyncService.run(new Runable() {
            public void run() {
                indexService.updateIndex(); // require self reference to use Spring caching proxy
            }
        });
    }
    return false;
}

@CachePut(cacheNames = INDEX_CACHE_NAME)
public Index updateIndex() {
    return calculateIndex();
}






caching