Въведение в кофеина

1. Въведение

В тази статия ще разгледаме кофеина - високоефективна кешираща библиотека за Java .

Една основна разлика между кеша и картата е, че кешът изхвърля съхранените елементи.

Една политика изгонване решава кои обекти следва да бъдат заличени във всеки даден момент. Тази политика пряко засяга скоростта на посещаемост на кеша - ключова характеристика на кеширащите библиотеки.

Кофеинът използва политиката за изселване на Window TinyLfu , която осигурява почти оптимална честота на удари .

2. Зависимост

Трябва да добавим зависимостта от кофеин към нашия pom.xml :

 com.github.ben-manes.caffeine caffeine 2.5.5 

Можете да намерите най-новата версия на кофеина в Maven Central.

3. Попълващ кеш

Нека се съсредоточим върху трите стратегии за кофеин за популация на кеш : ръчно, синхронно зареждане и асинхронно зареждане.

Първо, нека напишем клас за типовете стойности, които ще съхраняваме в нашия кеш:

class DataObject { private final String data; private static int objectCounter = 0; // standard constructors/getters public static DataObject get(String data) { objectCounter++; return new DataObject(data); } }

3.1. Ръчно попълване

В тази стратегия ние ръчно поставяме стойности в кеша и ги извличаме по-късно.

Нека инициализираме нашия кеш:

Cache cache = Caffeine.newBuilder() .expireAfterWrite(1, TimeUnit.MINUTES) .maximumSize(100) .build();

Сега можем да получим някаква стойност от кеша, използвайки getIfPresent метод . Този метод ще върне null, ако стойността не присъства в кеша:

String key = "A"; DataObject dataObject = cache.getIfPresent(key); assertNull(dataObject);

Можем да попълним кеша ръчно, използвайки метода put :

cache.put(key, dataObject); dataObject = cache.getIfPresent(key); assertNotNull(dataObject);

Можем също така да получите сумата, с помощта на GET метод , който отнема Функция заедно с ключ, като аргумент. Тази функция ще се използва за предоставяне на резервна стойност, ако ключът не присъства в кеша, който ще бъде вмъкнат в кеша след изчислението:

dataObject = cache .get(key, k -> DataObject.get("Data for A")); assertNotNull(dataObject); assertEquals("Data for A", dataObject.getData());

Методът get изчислява атомно. Това означава, че изчислението ще бъде направено само веднъж - дори ако няколко нишки поискат стойността едновременно. Ето защо използването на get е за предпочитане пред getIfPresent .

Понякога трябва да обезсилим някои кеширани стойности ръчно:

cache.invalidate(key); dataObject = cache.getIfPresent(key); assertNull(dataObject);

3.2. Синхронно зареждане

Този метод за зареждане на кеша отнема функция, която се използва за инициализиране на стойности, подобно на метода get на ръчната стратегия. Нека видим как можем да използваме това.

На първо място, трябва да инициализираме кеша си:

LoadingCache cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) .build(k -> DataObject.get("Data for " + k));

Сега можем да извлечем стойностите, използвайки метода get :

DataObject dataObject = cache.get(key); assertNotNull(dataObject); assertEquals("Data for " + key, dataObject.getData());

Също така можем да получим набор от стойности, използвайки метода getAll :

Map dataObjectMap = cache.getAll(Arrays.asList("A", "B", "C")); assertEquals(3, dataObjectMap.size());

Стойностите се извличат от основната функция за инициализация на фона , предадена на метода на изграждане . Това прави възможно използването на кеша като основна фасада за достъп до стойности.

3.3. Асинхронно зареждане

Тази стратегия работи по същия начин като предишната, но изпълнява операции асинхронно и връща CompletableFuture, съдържаща действителната стойност:

AsyncLoadingCache cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) .buildAsync(k -> DataObject.get("Data for " + k));

Можем да използваме методите get и getAll по същия начин, като вземем предвид факта, че те връщат CompletableFuture :

String key = "A"; cache.get(key).thenAccept(dataObject -> { assertNotNull(dataObject); assertEquals("Data for " + key, dataObject.getData()); }); cache.getAll(Arrays.asList("A", "B", "C")) .thenAccept(dataObjectMap -> assertEquals(3, dataObjectMap.size()));

CompletableFuture има богат и полезен API, за който можете да прочетете повече в тази статия.

4. Изселване на ценности

Кофеинът има три стратегии за изваждане на стойността : въз основа на размера, на времето и на референцията.

4.1. Изселване въз основа на размера

Този тип изселване предполага, че изселването настъпва при надвишаване на конфигурираната граница на размера на кеша . Има два начина за получаване на размера - преброяване на обекти в кеша или получаване на тежестите им.

Нека да видим как бихме могли да броим обекти в кеша . Когато кешът се инициализира, размерът му е равен на нула:

LoadingCache cache = Caffeine.newBuilder() .maximumSize(1) .build(k -> DataObject.get("Data for " + k)); assertEquals(0, cache.estimatedSize());

Когато добавим стойност, размерът очевидно се увеличава:

cache.get("A"); assertEquals(1, cache.estimatedSize());

Можем да добавим втората стойност към кеша, което води до премахването на първата стойност:

cache.get("B"); cache.cleanUp(); assertEquals(1, cache.estimatedSize());

Струва си да се спомене, че ние наричаме почистване метод, преди да се размера на кеша . Това е така, защото изваждането на кеша се изпълнява асинхронно и този метод помага да се изчака завършването на изваждането .

We can also pass a weigherFunctionto get the size of the cache:

LoadingCache cache = Caffeine.newBuilder() .maximumWeight(10) .weigher((k,v) -> 5) .build(k -> DataObject.get("Data for " + k)); assertEquals(0, cache.estimatedSize()); cache.get("A"); assertEquals(1, cache.estimatedSize()); cache.get("B"); assertEquals(2, cache.estimatedSize());

The values are removed from the cache when the weight is over 10:

cache.get("C"); cache.cleanUp(); assertEquals(2, cache.estimatedSize());

4.2. Time-Based Eviction

This eviction strategy is based on the expiration time of the entry and has three types:

  • Expire after access — entry is expired after period is passed since the last read or write occurs
  • Expire after write — entry is expired after period is passed since the last write occurs
  • Custom policy — an expiration time is calculated for each entry individually by the Expiry implementation

Let's configure the expire-after-access strategy using the expireAfterAccess method:

LoadingCache cache = Caffeine.newBuilder() .expireAfterAccess(5, TimeUnit.MINUTES) .build(k -> DataObject.get("Data for " + k));

To configure expire-after-write strategy, we use the expireAfterWrite method:

cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .weakKeys() .weakValues() .build(k -> DataObject.get("Data for " + k));

To initialize a custom policy, we need to implement the Expiry interface:

cache = Caffeine.newBuilder().expireAfter(new Expiry() { @Override public long expireAfterCreate( String key, DataObject value, long currentTime) { return value.getData().length() * 1000; } @Override public long expireAfterUpdate( String key, DataObject value, long currentTime, long currentDuration) { return currentDuration; } @Override public long expireAfterRead( String key, DataObject value, long currentTime, long currentDuration) { return currentDuration; } }).build(k -> DataObject.get("Data for " + k));

4.3. Reference-Based Eviction

We can configure our cache to allow garbage-collection of cache keys and/or values. To do this, we'd configure usage of the WeakRefence for both keys and values, and we can configure the SoftReference for garbage-collection of values only.

The WeakRefence usage allows garbage-collection of objects when there are not any strong references to the object. SoftReference allows objects to be garbage-collected based on the global Least-Recently-Used strategy of the JVM. More details about references in Java can be found here.

We should use Caffeine.weakKeys(), Caffeine.weakValues(), and Caffeine.softValues() to enable each option:

LoadingCache cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .weakKeys() .weakValues() .build(k -> DataObject.get("Data for " + k)); cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .softValues() .build(k -> DataObject.get("Data for " + k));

5. Refreshing

It's possible to configure the cache to refresh entries after a defined period automatically. Let's see how to do this using the refreshAfterWrite method:

Caffeine.newBuilder() .refreshAfterWrite(1, TimeUnit.MINUTES) .build(k -> DataObject.get("Data for " + k));

Here we should understand a difference between expireAfter and refreshAfter. When the expired entry is requested, an execution blocks until the new value would have been calculated by the build Function.

But if the entry is eligible for the refreshing, then the cache would return an old value and asynchronously reload the value.

6. Statistics

Caffeine has a means of recording statistics about cache usage:

LoadingCache cache = Caffeine.newBuilder() .maximumSize(100) .recordStats() .build(k -> DataObject.get("Data for " + k)); cache.get("A"); cache.get("A"); assertEquals(1, cache.stats().hitCount()); assertEquals(1, cache.stats().missCount());

We may also pass into recordStats supplier, which creates an implementation of the StatsCounter. This object will be pushed with every statistics-related change.

7. Conclusion

В тази статия се запознахме с библиотеката за кеширане на кофеин за Java. Видяхме как да конфигурираме и попълваме кеш памет, както и как да изберем подходяща политика за изтичане или опресняване според нашите нужди.

Показаният тук изходен код е достъпен в Github.