Panduan untuk Infinispan di Jawa

1. Gambaran keseluruhan

Dalam panduan ini, kita akan mengetahui mengenai Infinispan, sebuah kedai data kunci / nilai dalam memori yang dihantar dengan sekumpulan ciri yang lebih mantap daripada alat lain yang sama.

Untuk memahami cara kerjanya, kami akan membina projek mudah yang mempamerkan ciri-ciri yang paling umum dan menyemak bagaimana projek itu dapat digunakan.

2. Penyediaan Projek

Untuk dapat menggunakannya dengan cara ini, kita perlu menambahkan kebergantungannya di pom.xml kami .

Versi terbaru boleh didapati di repositori Maven Central:

 org.infinispan infinispan-core 9.1.5.Final 

Semua infrastruktur asas yang diperlukan akan dikendalikan secara teratur mulai sekarang.

3. Persediaan CacheManager

The CacheManager adalah asas majoriti ciri-ciri yang kami akan gunakan. Ia berfungsi sebagai wadah untuk semua cache yang dinyatakan, mengawal kitaran hayatnya, dan bertanggungjawab untuk konfigurasi global.

Infinispan kapal dengan cara yang sangat mudah untuk membina CacheManager :

public DefaultCacheManager cacheManager() { return new DefaultCacheManager(); }

Sekarang kita dapat membina cache dengan itu.

4. Penyediaan Cache

Cache ditentukan oleh nama dan konfigurasi. Konfigurasi yang diperlukan boleh dibina menggunakan kelas ConfigurationBuilder , yang sudah tersedia di classpath kami.

Untuk menguji cache kami, kami akan membina kaedah mudah yang mensimulasikan beberapa pertanyaan berat:

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!"; } }

Juga, untuk dapat memeriksa perubahan dalam cache kami, Infinispan memberikan penjelasan ringkas @Listener .

Semasa menentukan cache kami, kami dapat menyampaikan beberapa objek yang berminat dengan sebarang peristiwa yang terjadi di dalamnya, dan Infinispan akan memaklumkannya ketika menangani cache:

@Listener public class CacheListener { @CacheEntryCreated public void entryCreated(CacheEntryCreatedEvent event) { this.printLog("Adding key '" + event.getKey() + "' to cache", event); } @CacheEntryExpired public void entryExpired(CacheEntryExpiredEvent event) { this.printLog("Expiring key '" + event.getKey() + "' from cache", event); } @CacheEntryVisited public void entryVisited(CacheEntryVisitedEvent event) { this.printLog("Key '" + event.getKey() + "' was visited", event); } @CacheEntryActivated public void entryActivated(CacheEntryActivatedEvent event) { this.printLog("Activating key '" + event.getKey() + "' on cache", event); } @CacheEntryPassivated public void entryPassivated(CacheEntryPassivatedEvent event) { this.printLog("Passivating key '" + event.getKey() + "' from cache", event); } @CacheEntryLoaded public void entryLoaded(CacheEntryLoadedEvent event) { this.printLog("Loading key '" + event.getKey() + "' to cache", event); } @CacheEntriesEvicted public void entriesEvicted(CacheEntriesEvictedEvent 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); } } }

Sebelum mencetak mesej kami, kami memeriksa apakah peristiwa yang diberitahu sudah terjadi, kerana, untuk beberapa jenis acara, Infinispan mengirimkan dua pemberitahuan: satu sebelum dan satu tepat setelah diproses.

Sekarang mari kita bina kaedah untuk menangani pembuatan cache untuk kita:

private  Cache buildCache( String cacheName, DefaultCacheManager cacheManager, CacheListener listener, Configuration configuration) { cacheManager.defineConfiguration(cacheName, configuration); Cache cache = cacheManager.getCache(cacheName); cache.addListener(listener); return cache; }

Perhatikan bagaimana kita meneruskan konfigurasi ke CacheManager , dan kemudian gunakan cacheName yang sama untuk mendapatkan objek yang sesuai dengan cache yang diinginkan. Perhatikan juga bagaimana kita memberitahu pendengar ke objek cache itu sendiri.

Kami sekarang akan memeriksa lima konfigurasi cache yang berbeza, dan kita akan melihat bagaimana kita dapat mengaturnya dan memanfaatkannya dengan sebaik-baiknya.

4.1. Cache Ringkas

Jenis cache yang paling mudah dapat ditentukan dalam satu baris, menggunakan kaedah buildCache kami :

public Cache simpleHelloWorldCache( DefaultCacheManager cacheManager, CacheListener listener) { return this.buildCache(SIMPLE_HELLO_WORLD_CACHE, cacheManager, listener, new ConfigurationBuilder().build()); }

Kita sekarang boleh membina Perkhidmatan :

public String findSimpleHelloWorld() { String cacheKey = "simple-hello"; return simpleHelloWorldCache .computeIfAbsent(cacheKey, k -> repository.getHelloWorld()); }

Perhatikan bagaimana kita menggunakan cache, periksa dahulu apakah entri yang diinginkan sudah di-cache. Sekiranya tidak, kami perlu memanggil Repositori kami dan kemudian menyimpannya.

Mari tambahkan kaedah mudah dalam ujian kami untuk menentukan kaedah kami:

protected  long timeThis(Supplier supplier) { long millis = System.currentTimeMillis(); supplier.get(); return System.currentTimeMillis() - millis; }

Mengujinya, kita dapat memeriksa waktu antara melaksanakan dua panggilan kaedah:

@Test public void whenGetIsCalledTwoTimes_thenTheSecondShouldHitTheCache() { assertThat(timeThis(() -> helloWorldService.findSimpleHelloWorld())) .isGreaterThanOrEqualTo(1000); assertThat(timeThis(() -> helloWorldService.findSimpleHelloWorld())) .isLessThan(100); }

4.2. Cache Tamat

Kita dapat menentukan cache di mana semua entri mempunyai jangka hayat, dengan kata lain, elemen akan dikeluarkan dari cache setelah jangka waktu tertentu. Konfigurasi agak mudah:

private Configuration expiringConfiguration() { return new ConfigurationBuilder().expiration() .lifespan(1, TimeUnit.SECONDS) .build(); }

Sekarang kita membina cache menggunakan konfigurasi di atas:

public Cache expiringHelloWorldCache( DefaultCacheManager cacheManager, CacheListener listener) { return this.buildCache(EXPIRING_HELLO_WORLD_CACHE, cacheManager, listener, expiringConfiguration()); }

Dan akhirnya, gunakannya dengan kaedah yang serupa dari cache mudah kami di atas:

public String findSimpleHelloWorldInExpiringCache() { String cacheKey = "simple-hello"; String helloWorld = expiringHelloWorldCache.get(cacheKey); if (helloWorld == null) { helloWorld = repository.getHelloWorld(); expiringHelloWorldCache.put(cacheKey, helloWorld); } return helloWorld; }

Mari kita menguji masa kita lagi:

@Test public void whenGetIsCalledTwoTimesQuickly_thenTheSecondShouldHitTheCache() { assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld())) .isGreaterThanOrEqualTo(1000); assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld())) .isLessThan(100); }

Menjalankannya, kita melihat bahawa berturut-turut cache berjaya. Untuk mempamerkan yang tamat tempoh adalah relatif kepada kemasukannya put masa, mari pasukan itu dalam kemasukan kami:

@Test public void whenGetIsCalledTwiceSparsely_thenNeitherHitsTheCache() throws InterruptedException { assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld())) .isGreaterThanOrEqualTo(1000); Thread.sleep(1100); assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld())) .isGreaterThanOrEqualTo(1000); }

Setelah menjalankan ujian, perhatikan bagaimana selepas masa yang ditentukan entri kami habis dari cache. Kami dapat mengesahkannya dengan melihat garisan log yang dicetak dari pendengar kami:

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

Perhatikan bahawa entri akan habis masa ketika kami cuba mengaksesnya. Infinispan memeriksa entri yang telah habis masa berlakunya dalam dua saat: ketika kami mencuba untuk mengaksesnya atau ketika benang penuai mengimbas cache.

Kita boleh menggunakan kadaluarsa walaupun dalam cache tanpa konfigurasi utamanya. Kaedah meletakkan menerima lebih banyak hujah:

simpleHelloWorldCache.put(cacheKey, helloWorld, 10, TimeUnit.SECONDS);

Atau, bukannya jangka hayat yang tetap, kita boleh memberikan idleTime maksimum pada entri kita :

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.

4.3. Cache Eviction

In Infinispan we can limit the number of entries in a given cache with the eviction configuration:

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

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.

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.

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

Infinispan dihantar dengan kawalan transaksi yang kuat. Seperti rakan pangkalan data, ia berguna untuk mengekalkan integriti sementara lebih daripada satu utas berusaha menulis entri yang sama.

Mari lihat bagaimana kita dapat menentukan cache dengan keupayaan transaksi:

private Configuration transactionalConfiguration() { return new ConfigurationBuilder() .transaction().transactionMode(TransactionMode.TRANSACTIONAL) .lockingMode(LockingMode.PESSIMISTIC) .build(); }

Untuk memungkinkan untuk mengujinya, mari kita bina dua kaedah - satu yang menyelesaikan transaksi dengan pantas, dan yang memerlukan beberapa saat:

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

Sekarang mari buat ujian yang melaksanakan kedua kaedah dan periksa bagaimana tingkah laku 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); }

Melaksanakannya, kita akan melihat aktiviti berikut di konsol kita lagi:

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

Periksa masa di utas utama, tunggu akhir transaksi yang dibuat dengan kaedah perlahan.

5. Kesimpulan

Dalam artikel ini, kami telah melihat apa itu Infinispan, dan ciri dan keupayaan utama sebagai cache dalam aplikasi.

Seperti biasa, kodnya boleh didapati di Github.