1. Overview
1.概述
In this guide, we’ll learn about Infinispan, an in-memory key/value data store that ships with a more robust set of features than other tools of the same niche.
在本指南中,我们将了解Infinispan,这是一个内存中的键/值数据存储,与其他同类型的工具相比,它具有更强大的功能集。
To understand how it works, we’ll build a simple project showcasing the most common features and check how they can be used.
为了了解它是如何工作的,我们将建立一个简单的项目,展示最常见的功能,并检查它们如何被使用。
2. Project Setup
2.项目设置
To be able to use it this way, we’ll need to add it’s dependency in our pom.xml.
为了能够以这种方式使用它,我们需要在我们的pom.xml中添加它的依赖性。
The latest version can be found in Maven Central repository:
最新版本可以在Maven Central资源库中找到。
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
<version>9.1.5.Final</version>
</dependency>
All the necessary underlying infrastructure will be handled programmatically from now on.
从现在开始,所有必要的底层基础设施都将以编程方式处理。
3. CacheManager Setup
3.CacheManager设置
The CacheManager is the foundation of the majority of features that we’ll use. It acts as a container for all declared caches, controlling their lifecycle, and is responsible for the global configuration.
CacheManager是我们将使用的大部分功能的基础。它充当了所有已声明的缓存的容器,控制它们的生命周期,并负责全局配置。
Infinispan ships with a really easy way to build the CacheManager:
Infinispan提供了一个非常简单的方法来构建CacheManager。
public DefaultCacheManager cacheManager() {
return new DefaultCacheManager();
}
Now we’re able to build our caches with it.
现在我们能够用它来建立我们的缓存。
4. Caches Setup
4.缓存设置
A cache is defined by a name and a configuration. The necessary configuration can be built using the class ConfigurationBuilder, already available in our classpath.
一个缓存由一个名字和一个配置来定义。必要的配置可以使用ConfigurationBuilder类来建立,该类已经在我们的classpath中可用。
To test our caches, we’ll build a simple method that simulates some heavy query:
为了测试我们的缓存,我们将建立一个简单的方法来模拟一些重度查询。
public class HelloWorldRepository {
public String getHelloWorld() {
try {
System.out.println("Executing some heavy query");
Thread.sleep(1000);
} catch (InterruptedException e) {
// ...
e.printStackTrace();
}
return "Hello World!";
}
}
Also, to be able to check for changes in our caches, Infinispan provides a simple annotation @Listener.
另外,为了能够检查我们缓存中的变化,Infinispan提供了一个简单的注解@Listener。
When defining our cache, we can pass some object interested in any event happening inside it, and Infinispan will notify it when handling the cache:
在定义我们的缓存时,我们可以传递一些对里面发生的任何事件感兴趣的对象,Infinispan会在处理缓存时通知它。
@Listener
public class CacheListener {
@CacheEntryCreated
public void entryCreated(CacheEntryCreatedEvent<String, String> event) {
this.printLog("Adding key '" + event.getKey()
+ "' to cache", event);
}
@CacheEntryExpired
public void entryExpired(CacheEntryExpiredEvent<String, String> event) {
this.printLog("Expiring key '" + event.getKey()
+ "' from cache", event);
}
@CacheEntryVisited
public void entryVisited(CacheEntryVisitedEvent<String, String> event) {
this.printLog("Key '" + event.getKey() + "' was visited", event);
}
@CacheEntryActivated
public void entryActivated(CacheEntryActivatedEvent<String, String> event) {
this.printLog("Activating key '" + event.getKey()
+ "' on cache", event);
}
@CacheEntryPassivated
public void entryPassivated(CacheEntryPassivatedEvent<String, String> event) {
this.printLog("Passivating key '" + event.getKey()
+ "' from cache", event);
}
@CacheEntryLoaded
public void entryLoaded(CacheEntryLoadedEvent<String, String> event) {
this.printLog("Loading key '" + event.getKey()
+ "' to cache", event);
}
@CacheEntriesEvicted
public void entriesEvicted(CacheEntriesEvictedEvent<String, String> event) {
StringBuilder builder = new StringBuilder();
event.getEntries().forEach(
(key, value) -> builder.append(key).append(", "));
System.out.println("Evicting following entries from cache: "
+ builder.toString());
}
private void printLog(String log, CacheEntryEvent event) {
if (!event.isPre()) {
System.out.println(log);
}
}
}
Before printing our message we check if the event being notified already has happened, because, for some event types, Infinispan sends two notifications: one before and one right after it has been processed.
在打印我们的消息之前,我们要检查被通知的事件是否已经发生,因为对于某些事件类型,Infinispan会发送两个通知:一个在它被处理之前,一个在它被处理之后。
Now let’s build a method to handle the cache creation for us:
现在让我们建立一个方法来为我们处理缓冲区的创建。
private <K, V> Cache<K, V> buildCache(
String cacheName,
DefaultCacheManager cacheManager,
CacheListener listener,
Configuration configuration) {
cacheManager.defineConfiguration(cacheName, configuration);
Cache<K, V> cache = cacheManager.getCache(cacheName);
cache.addListener(listener);
return cache;
}
Notice how we pass a configuration to CacheManager, and then use the same cacheName to get the object corresponding to the wanted cache. Note also how we inform the listener to the cache object itself.
注意我们是如何将一个配置传递给CacheManager,然后使用相同的cacheName来获取与所需缓存对应的对象。还要注意我们是如何通知监听器到缓存对象本身的。
We’ll now check five different cache configurations, and we’ll see how we can set them up and make the best use of them.
现在我们将检查五种不同的缓存配置,我们将看到我们如何设置它们并充分利用它们。
4.1. Simple Cache
4.1.简单缓存
The simplest type of cache can be defined in one line, using our method buildCache:
最简单的缓存类型可以在一行中定义,使用我们的方法buildCache。
public Cache<String, String> simpleHelloWorldCache(
DefaultCacheManager cacheManager,
CacheListener listener) {
return this.buildCache(SIMPLE_HELLO_WORLD_CACHE,
cacheManager, listener, new ConfigurationBuilder().build());
}
We can now build a Service:
我们现在可以建立一个服务。
public String findSimpleHelloWorld() {
String cacheKey = "simple-hello";
return simpleHelloWorldCache
.computeIfAbsent(cacheKey, k -> repository.getHelloWorld());
}
Note how we use the cache, first checking if the wanted entry is already cached. If it isn’t, we’ll need to call our Repository and then cache it.
注意我们是如何使用缓存的,首先检查想要的条目是否已经被缓存了。如果没有,我们需要调用我们的Repository,然后进行缓存。
Let’s add a simple method in our tests to time our methods:
让我们在测试中添加一个简单的方法来为我们的方法计时。
protected <T> long timeThis(Supplier<T> supplier) {
long millis = System.currentTimeMillis();
supplier.get();
return System.currentTimeMillis() - millis;
}
Testing it, we can check the time between executing two method calls:
测试它,我们可以检查执行两个方法调用之间的时间。
@Test
public void whenGetIsCalledTwoTimes_thenTheSecondShouldHitTheCache() {
assertThat(timeThis(() -> helloWorldService.findSimpleHelloWorld()))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(() -> helloWorldService.findSimpleHelloWorld()))
.isLessThan(100);
}
4.2. Expiration Cache
4.2.过期缓存
We can define a cache in which all entries have a lifespan, in other words, elements will be removed from the cache after a given period. The configuration is quite simple:
我们可以定义一个缓存,其中所有的条目都有一个寿命,换句话说,元素在给定的时间后将从缓存中删除。这个配置很简单。
private Configuration expiringConfiguration() {
return new ConfigurationBuilder().expiration()
.lifespan(1, TimeUnit.SECONDS)
.build();
}
Now we build our cache using the above configuration:
现在我们使用上述配置建立我们的缓存。
public Cache<String, String> expiringHelloWorldCache(
DefaultCacheManager cacheManager,
CacheListener listener) {
return this.buildCache(EXPIRING_HELLO_WORLD_CACHE,
cacheManager, listener, expiringConfiguration());
}
And finally, use it in a similar method from our simple cache above:
最后,用我们上面的简单缓存的类似方法来使用它。
public String findSimpleHelloWorldInExpiringCache() {
String cacheKey = "simple-hello";
String helloWorld = expiringHelloWorldCache.get(cacheKey);
if (helloWorld == null) {
helloWorld = repository.getHelloWorld();
expiringHelloWorldCache.put(cacheKey, helloWorld);
}
return helloWorld;
}
Let’s test our times again:
让我们再次测试一下我们的时间。
@Test
public void whenGetIsCalledTwoTimesQuickly_thenTheSecondShouldHitTheCache() {
assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
.isLessThan(100);
}
Running it, we see that in quick succession the cache hits. To showcase that the expiration is relative to its entry put time, let’s force it in our entry:
运行它,我们看到,在快速连续的缓存命中。为了展示过期时间是相对于它的条目put时间而言的,让我们在我们的条目中强制它。
@Test
public void whenGetIsCalledTwiceSparsely_thenNeitherHitsTheCache()
throws InterruptedException {
assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
.isGreaterThanOrEqualTo(1000);
Thread.sleep(1100);
assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
.isGreaterThanOrEqualTo(1000);
}
After running the test, note how after the given time our entry was expired from the cache. We can confirm this by looking at the printed log lines from our listener:
运行测试后,注意到在给定的时间后,我们的条目从缓存中过期了。我们可以通过查看我们的监听器打印的日志行来确认这一点。
Executing some heavy query
Adding key 'simple-hello' to cache
Expiring key 'simple-hello' from cache
Executing some heavy query
Adding key 'simple-hello' to cache
Note that the entry is expired when we try to access it. Infinispan checks for an expired entry in two moments: when we try to access it or when the reaper thread scans the cache.
请注意,当我们试图访问它时,该条目已经过期。Infinispan在两个时刻检查过期的条目:当我们试图访问它时或当reaper线程扫描缓存时。
We can use expiration even in caches without it in their main configuration. The method put accepts more arguments:
我们可以在缓存中使用过期,甚至在他们的主要配置中没有它。方法put接受了更多的参数。
simpleHelloWorldCache.put(cacheKey, helloWorld, 10, TimeUnit.SECONDS);
Or, instead of a fixed lifespan, we can give our entry a maximum idleTime:
或者,我们可以给我们的条目一个最大的idleTime,而不是一个固定的生命期。
simpleHelloWorldCache.put(cacheKey, helloWorld, -1, TimeUnit.SECONDS, 10, TimeUnit.SECONDS);
Using -1 to the lifespan attribute, the cache won’t suffer expiration from it, but when we combine it with 10 seconds of idleTime, we tell Infinispan to expire this entry unless it is visited in this timeframe.
使用-1的lifespan属性,缓存不会因此而过期,但是当我们把它和10秒的idleTime结合起来时,我们告诉Infinispan,除非在这个时间范围内被访问,否则这个条目将过期。
4.3. Cache Eviction
4.3.缓存驱逐
In Infinispan we can limit the number of entries in a given cache with the eviction configuration:
在Infinispan中,我们可以通过eviction配置来限制特定缓存中的条目数量:。
private Configuration evictingConfiguration() {
return new ConfigurationBuilder()
.memory().evictionType(EvictionType.COUNT).size(1)
.build();
}
In this example, we’re limiting the maximum entries in this cache to one, meaning that, if we try to enter another one, it’ll be evicted from our cache.
在这个例子中,我们将这个缓存的最大条目限制为一个,这意味着,如果我们试图输入另一个条目,它将被从我们的缓存中驱逐。
Again, the method is similar to the already presented here:
同样,该方法与这里已经提出的方法类似。
public String findEvictingHelloWorld(String key) {
String value = evictingHelloWorldCache.get(key);
if(value == null) {
value = repository.getHelloWorld();
evictingHelloWorldCache.put(key, value);
}
return value;
}
Let’s build our test:
让我们建立我们的测试。
@Test
public void whenTwoAreAdded_thenFirstShouldntBeAvailable() {
assertThat(timeThis(
() -> helloWorldService.findEvictingHelloWorld("key 1")))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(
() -> helloWorldService.findEvictingHelloWorld("key 2")))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(
() -> helloWorldService.findEvictingHelloWorld("key 1")))
.isGreaterThanOrEqualTo(1000);
}
Running the test, we can look at our listener log of activities:
运行测试,我们可以看一下我们的监听器的活动日志。
Executing some heavy query
Adding key 'key 1' to cache
Executing some heavy query
Evicting following entries from cache: key 1,
Adding key 'key 2' to cache
Executing some heavy query
Evicting following entries from cache: key 2,
Adding key 'key 1' to cache
Check how the first key was automatically removed from the cache when we inserted the second one, and then, the second one removed also to give room for our first key again.
看看当我们插入第二把钥匙时,第一把钥匙是如何从缓存中自动删除的,然后,第二把钥匙也被删除,为我们的第一把钥匙再次提供了空间。
4.4. Passivation Cache
4.4.钝化缓存
The cache passivation is one of the powerful features of Infinispan. By combining passivation and eviction, we can create a cache that doesn’t occupy a lot of memory, without losing information.
缓存钝化是Infinispan的强大功能之一。通过结合钝化和驱逐,我们可以创建一个不占用大量内存的缓存,而不会丢失信息。
Let’s have a look at a passivation configuration:
让我们看一下钝化配置。
private Configuration passivatingConfiguration() {
return new ConfigurationBuilder()
.memory().evictionType(EvictionType.COUNT).size(1)
.persistence()
.passivation(true) // activating passivation
.addSingleFileStore() // in a single file
.purgeOnStartup(true) // clean the file on startup
.location(System.getProperty("java.io.tmpdir"))
.build();
}
We’re again forcing just one entry in our cache memory, but telling Infinispan to passivate the remaining entries, instead of just removing them.
我们再次强制在我们的缓存内存中只有一个条目,但告诉Infinispan使其余的条目被激活,而不是仅仅删除它们。
Let’s see what happens when we try to fill more than one entry:
让我们看看当我们试图填补一个以上的条目时会发生什么。
public String findPassivatingHelloWorld(String key) {
return passivatingHelloWorldCache.computeIfAbsent(key, k ->
repository.getHelloWorld());
}
Let’s build our test and run it:
让我们建立我们的测试并运行它。
@Test
public void whenTwoAreAdded_thenTheFirstShouldBeAvailable() {
assertThat(timeThis(
() -> helloWorldService.findPassivatingHelloWorld("key 1")))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(
() -> helloWorldService.findPassivatingHelloWorld("key 2")))
.isGreaterThanOrEqualTo(1000);
assertThat(timeThis(
() -> helloWorldService.findPassivatingHelloWorld("key 1")))
.isLessThan(100);
}
Now let’s look at our listener activities:
现在我们来看看我们的听众活动。
Executing some heavy query
Adding key 'key 1' to cache
Executing some heavy query
Passivating key 'key 1' from cache
Evicting following entries from cache: key 1,
Adding key 'key 2' to cache
Passivating key 'key 2' from cache
Evicting following entries from cache: key 2,
Loading key 'key 1' to cache
Activating key 'key 1' on cache
Key 'key 1' was visited
Note how many steps did it take to keep our cache with only one entry. Also, note the order of steps – passivation, eviction and then loading followed by activation. Let’s see what those steps mean:
请注意,我们的缓存只保留了一个条目,这需要多少个步骤。另外,注意步骤的顺序–钝化、驱逐,然后是加载,接着是激活。让我们看看这些步骤意味着什么。
- Passivation – our entry is stored in another place, away from the mains storage of Infinispan (in this case, the memory)
- Eviction – the entry is removed, to free memory and to keep the configured maximum number of entries in the cache
- Loading – when trying to reach our passivated entry, Infinispan checks it’s stored contents and load the entry to the memory again
- Activation – the entry is now accessible in Infinispan again
4.5. Transactional Cache
4.5.事务性高速缓存
Infinispan ships with a powerful transaction control. Like the database counterpart, it is useful in maintaining integrity while more than one thread is trying to write the same entry.
Infinispan提供了一个强大的事务控制。就像数据库的对应控件一样,它在多个线程试图写同一个条目时保持完整性很有用。
Let’s see how we can define a cache with transactional capabilities:
让我们看看如何定义一个具有交易功能的缓存。
private Configuration transactionalConfiguration() {
return new ConfigurationBuilder()
.transaction().transactionMode(TransactionMode.TRANSACTIONAL)
.lockingMode(LockingMode.PESSIMISTIC)
.build();
}
To make it possible to test it, let’s build two methods – one that finishes its transaction rapidly, and one that takes a while:
为了使测试成为可能,让我们建立两个方法–一个迅速完成交易,另一个则需要一段时间。
public Integer getQuickHowManyVisits() {
TransactionManager tm = transactionalCache
.getAdvancedCache().getTransactionManager();
tm.begin();
Integer howManyVisits = transactionalCache.get(KEY);
howManyVisits++;
System.out.println("I'll try to set HowManyVisits to " + howManyVisits);
StopWatch watch = new StopWatch();
watch.start();
transactionalCache.put(KEY, howManyVisits);
watch.stop();
System.out.println("I was able to set HowManyVisits to " + howManyVisits +
" after waiting " + watch.getTotalTimeSeconds() + " seconds");
tm.commit();
return howManyVisits;
}
public void startBackgroundBatch() {
TransactionManager tm = transactionalCache
.getAdvancedCache().getTransactionManager();
tm.begin();
transactionalCache.put(KEY, 1000);
System.out.println("HowManyVisits should now be 1000, " +
"but we are holding the transaction");
Thread.sleep(1000L);
tm.rollback();
System.out.println("The slow batch suffered a rollback");
}
Now let’s create a test that executes both methods and check how Infinispan will behave:
现在让我们创建一个测试,执行这两种方法,并检查Infinispan将如何表现。
@Test
public void whenLockingAnEntry_thenItShouldBeInaccessible() throws InterruptedException {
Runnable backGroundJob = () -> transactionalService.startBackgroundBatch();
Thread backgroundThread = new Thread(backGroundJob);
transactionalService.getQuickHowManyVisits();
backgroundThread.start();
Thread.sleep(100); //lets wait our thread warm up
assertThat(timeThis(() -> transactionalService.getQuickHowManyVisits()))
.isGreaterThan(500).isLessThan(1000);
}
Executing it, we’ll see the following activities in our console again:
执行它,我们将在控制台中再次看到以下活动。
Adding key 'key' to cache
Key 'key' was visited
Ill try to set HowManyVisits to 1
I was able to set HowManyVisits to 1 after waiting 0.001 seconds
HowManyVisits should now be 1000, but we are holding the transaction
Key 'key' was visited
Ill try to set HowManyVisits to 2
I was able to set HowManyVisits to 2 after waiting 0.902 seconds
The slow batch suffered a rollback
Check the time on the main thread, waiting for the end of the transaction created by the slow method.
检查主线程上的时间,等待由慢速方法创建的事务结束。
5. Conclusion
5.结论
In this article, we’ve seen what Infinispan is, and it’s leading features and capabilities as a cache within an application.
在这篇文章中,我们已经看到了什么是Infinispan,以及它作为应用程序中的缓存的主要特点和能力。
As always, the code can be found over on Github.
一如既往,代码可以在Github上找到over。