Pengenalan Kafein

1. Pengenalan

Dalam artikel ini, kita akan melihat Kafein - perpustakaan cache berprestasi tinggi untuk Java .

Satu perbezaan mendasar antara cache dan Peta adalah bahawa cache mengusir item yang disimpan.

An dasar pengusiran memutuskan objek patut dihapuskan pada bila-bila masa. Dasar ini secara langsung mempengaruhi kadar hit cache - ciri penting perpustakaan cache.

Kafein menggunakan dasar pengusiran Window TinyLfu , yang memberikan kadar hit hampir optimum .

2. Kebergantungan

Kita perlu menambahkan kebergantungan kafein ke pom.xml kami :

 com.github.ben-manes.caffeine caffeine 2.5.5 

Anda boleh mendapatkan versi terbaru kafein di Maven Central.

3. Mengisi Cache

Mari fokus pada tiga strategi Caffeine untuk populasi cache : pemuatan manual, sinkronisasi, dan pemuatan tak segerak.

Pertama, mari tulis kelas untuk jenis nilai yang akan kami simpan di cache kami:

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. Mengisi Manual

Dalam strategi ini, kami memasukkan nilai ke dalam cache secara manual dan mengambilnya kemudian.

Mari mulakan cache kami:

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

Sekarang, kita dapat memperoleh beberapa nilai dari cache menggunakan kaedah getIfPresent . Kaedah ini akan mengembalikan nol jika nilainya tidak ada dalam cache:

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

Kami dapat mengisi cache secara manual menggunakan kaedah put :

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

Kita juga dapat memperoleh nilai menggunakan metode get , yang menggunakan Fungsi bersama dengan kunci sebagai argumen. Fungsi ini akan digunakan untuk memberikan nilai mundur jika kunci tidak ada di cache, yang akan dimasukkan ke dalam cache setelah pengiraan:

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

The get kaedah melakukan pengiraan atom. Ini bermaksud bahawa pengiraan akan dibuat hanya sekali - walaupun beberapa utas meminta nilainya secara serentak. Itulah sebabnya menggunakan get lebih disukai daripada getIfPresent .

Kadang-kadang kita perlu membatalkan beberapa nilai cache secara manual:

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

3.2. Pemuatan segerak

Kaedah memuatkan cache ini mengambil Fungsi, yang digunakan untuk menginisialisasi nilai, mirip dengan metode get dari strategi manual. Mari lihat bagaimana kita boleh menggunakannya.

Pertama sekali, kita perlu memulakan cache kita:

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

Sekarang kita dapat mengambil nilai menggunakan kaedah get :

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

Kami juga dapat memperoleh satu set nilai menggunakan kaedah getAll :

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

Nilai diambil dari fungsi inisial back-end yang mendasari yang diteruskan ke kaedah build . Ini memungkinkan untuk menggunakan cache sebagai fasad utama untuk mengakses nilai.

3.3. Memuat Asinkron

Strategi ini berfungsi sama seperti yang sebelumnya tetapi melakukan operasi secara tidak segerak dan mengembalikan sebuah CompletableFuture yang memegang nilai sebenarnya:

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

Kita boleh menggunakan get dan getAll kaedah , dengan cara yang sama, dengan mengambil kira hakikat bahawa mereka kembali 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 mempunyai API yang kaya dan berguna, yang boleh anda baca lebih lanjut dalam artikel ini.

4. Penggabungan Nilai

Kafein mempunyai tiga strategi untuk pengusiran nilai : berdasarkan ukuran, berdasarkan masa, dan berdasarkan rujukan.

4.1. Pengusiran Berdasarkan Ukuran

Pengusiran jenis ini mengandaikan bahawa pengusiran berlaku apabila had ukuran cache yang dikonfigurasikan terlampaui . Terdapat dua cara untuk mendapatkan ukuran - menghitung objek di cache, atau mendapatkan bobotnya.

Mari lihat bagaimana kita dapat mengira objek dalam cache . Apabila cache diinisialisasi, ukurannya sama dengan sifar:

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

Apabila kita menambah nilai, ukurannya jelas meningkat:

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

Kami dapat menambahkan nilai kedua ke cache, yang membawa kepada penghapusan nilai pertama:

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

Perlu disebutkan bahawa kita memanggil kaedah cleanUp sebelum mendapatkan ukuran cache . Ini kerana pengusiran cache dijalankan secara tidak segerak, dan kaedah ini membantu menunggu penyelesaian pengusiran .

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

Dalam artikel ini, kami berkenalan dengan perpustakaan cache Cache untuk Java. Kami melihat cara mengkonfigurasi dan mengisi cache, serta cara memilih polisi tamat atau menyegarkan yang sesuai mengikut keperluan kami.

Kod sumber yang ditunjukkan di sini boleh didapati di Github.