Panduan ke Infinispan di Java

1. Ikhtisar

Dalam panduan ini, kita akan belajar tentang Infinispan, penyimpanan data kunci / nilai dalam memori yang dikirimkan dengan serangkaian fitur yang lebih kuat daripada alat lain dari ceruk yang sama.

Untuk memahami cara kerjanya, kami akan membuat proyek sederhana yang menampilkan fitur paling umum dan memeriksa bagaimana fitur tersebut dapat digunakan.

2. Pengaturan Proyek

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

Versi terbaru dapat ditemukan di repositori Maven Central:

 org.infinispan infinispan-core 9.1.5.Final 

Semua infrastruktur dasar yang diperlukan akan ditangani secara terprogram mulai sekarang.

3. Penyiapan CacheManager

The CacheManager adalah dasar dari sebagian besar fitur yang akan kami gunakan. Ini bertindak sebagai wadah untuk semua cache yang dideklarasikan, mengontrol siklus hidup mereka, dan bertanggung jawab atas konfigurasi global.

Infinispan dikirimkan dengan cara yang sangat mudah untuk membangun CacheManager :

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

Sekarang kami dapat membangun cache kami dengannya.

4. Pengaturan Cache

Cache ditentukan oleh nama dan konfigurasi. Konfigurasi yang diperlukan dapat dibangun menggunakan kelas ConfigurationBuilder , yang sudah tersedia di classpath kita.

Untuk menguji cache kami, kami akan membuat metode sederhana yang mensimulasikan beberapa kueri 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!"; } }

Selain itu, untuk dapat memeriksa perubahan di cache kami, Infinispan memberikan anotasi sederhana @Listener .

Saat menentukan cache kita, kita dapat meneruskan beberapa objek yang tertarik dengan peristiwa apa pun yang terjadi di dalamnya, dan Infinispan akan memberi tahu saat 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 pesan kami, kami memeriksa apakah acara yang diberitahukan sudah terjadi, karena, untuk beberapa jenis acara, Infinispan mengirimkan dua notifikasi: satu sebelum dan satu tepat setelah diproses.

Sekarang mari kita membangun metode 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 , lalu menggunakan cacheName yang sama untuk mendapatkan objek yang sesuai dengan cache yang diinginkan. Perhatikan juga bagaimana kami memberi tahu pendengar ke objek cache itu sendiri.

Sekarang kita akan memeriksa lima konfigurasi cache yang berbeda, dan kita akan melihat bagaimana kita dapat mengaturnya dan memanfaatkannya sebaik mungkin.

4.1. Cache Sederhana

Jenis cache yang paling sederhana dapat ditentukan dalam satu baris, menggunakan metode buildCache :

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

Kami sekarang dapat membangun Layanan :

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

Perhatikan bagaimana kami menggunakan cache, pertama-tama periksa apakah entri yang diinginkan sudah di-cache. Jika tidak, kita perlu memanggil Repositori kita dan kemudian menyimpannya dalam cache.

Mari tambahkan metode sederhana dalam pengujian kami untuk mengatur waktu metode kami:

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

Mengujinya, kita dapat memeriksa waktu antara menjalankan dua panggilan metode:

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

4.2. Cache Kedaluwarsa

Kita dapat menentukan cache di mana semua entri memiliki umur, dengan kata lain, elemen akan dihapus dari cache setelah jangka waktu tertentu. Konfigurasinya cukup sederhana:

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

Sekarang kami membangun cache kami menggunakan konfigurasi di atas:

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

Dan terakhir, gunakan dengan metode serupa dari cache sederhana 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 uji waktu kita lagi:

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

Dengan menjalankannya, kita melihat bahwa secara berurutan cache berhasil. Untuk memamerkan bahwa berakhirnya relatif terhadap masuknya put waktu, biarkan kekuatan ini dalam entri 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 pengujian, perhatikan bagaimana setelah waktu tertentu entri kami kedaluwarsa dari cache. Kami dapat mengonfirmasi ini dengan melihat baris 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 bahwa entri tersebut kedaluwarsa saat kami mencoba mengaksesnya. Infinispan memeriksa entri yang kedaluwarsa dalam dua saat: ketika kami mencoba mengaksesnya atau ketika thread penuai memindai cache.

Kami dapat menggunakan kedaluwarsa bahkan di cache tanpa itu dalam konfigurasi utamanya. Metode put menerima lebih banyak argumen:

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

Atau, alih-alih umur tetap, kami dapat memberikan entri kami idleTime maksimum :

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 dikirimkan dengan kontrol transaksi yang kuat. Seperti rekan database, ini berguna dalam menjaga integritas sementara lebih dari satu utas mencoba menulis entri yang sama.

Mari kita lihat bagaimana kita dapat mendefinisikan cache dengan kemampuan transaksional:

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

Agar memungkinkan untuk mengujinya, mari buat dua metode - satu yang menyelesaikan transaksinya dengan cepat, dan yang lain membutuhkan 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 kita buat tes yang mengeksekusi kedua metode dan memeriksa bagaimana Infinispan akan berperilaku:

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

Saat menjalankannya, kita akan melihat aktivitas 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 waktu di utas utama, menunggu akhir transaksi dibuat dengan metode lambat.

5. Kesimpulan

Pada artikel ini, kita telah melihat apa itu Infinispan, dan fitur serta kemampuannya sebagai cache dalam aplikasi.

Seperti biasa, kode dapat ditemukan di Github.