Pengantar Kafein

1. Perkenalan

Pada artikel ini, kita akan melihat Caffeine - pustaka cache berkinerja tinggi untuk Java .

Satu perbedaan mendasar antara cache dan Map adalah cache mengusir item yang disimpan.

Sebuah kebijakan penggusuran memutuskan mana objek harus dihapus pada waktu tertentu. Kebijakan ini secara langsung memengaruhi tingkat ketepatan cache - karakteristik penting dari pustaka cache.

Caffeine menggunakan kebijakan pengusiran TinyLfu Window , yang memberikan rasio klik yang hampir optimal .

2. Ketergantungan

Kita perlu menambahkan ketergantungan kafein ke pom.xml kita :

 com.github.ben-manes.caffeine caffeine 2.5.5 

Anda dapat menemukan versi terbaru kafein di Maven Central.

3. Mengisi Cache

Mari fokus pada tiga strategi Caffeine untuk populasi cache : manual, pemuatan sinkron, dan pemuatan asinkron.

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

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

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

Mari kita inisialisasi cache kita:

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

Sekarang, kita bisa mendapatkan beberapa nilai dari cache menggunakan metode getIfPresent . Metode ini akan mengembalikan null jika nilainya tidak ada di cache:

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

Kita dapat mengisi cache secara manual menggunakan metode put :

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

Kita juga bisa mendapatkan nilai menggunakan metode get , yang menggunakan Fungsi bersama dengan kunci sebagai argumen. Fungsi ini akan digunakan untuk memberikan nilai fallback jika kunci tidak ada di cache, yang akan disisipkan ke dalam cache setelah komputasi:

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

The get Metode melakukan perhitungan atom. Artinya, penghitungan hanya akan dilakukan sekali - meskipun beberapa thread meminta nilainya secara bersamaan. Itulah mengapa menggunakan get lebih disukai daripada getIfPresent .

Terkadang kami perlu membuat beberapa nilai cache secara manual tidak valid :

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

3.2. Pemuatan Sinkron

Metode memuat cache ini mengambil sebuah Fungsi, yang digunakan untuk menginisialisasi nilai, mirip dengan metode get dari strategi manual. Mari kita lihat bagaimana kita bisa menggunakannya.

Pertama-tama, kita perlu menginisialisasi 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 metode get :

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

Kita juga bisa mendapatkan satu set nilai menggunakan metode getAll :

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

Nilai diambil dari Fungsi inisialisasi back-end yang mendasari yang diteruskan ke metode build . Ini memungkinkan untuk menggunakan cache sebagai fasad utama untuk mengakses nilai.

3.3. Pemuatan Asinkron

Strategi ini bekerja sama seperti sebelumnya tetapi melakukan operasi secara asinkron dan menampilkan CompletableFuture yang memiliki nilai sebenarnya:

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

Kita dapat menggunakan metode get dan getAll , dengan cara yang sama, dengan mempertimbangkan fakta bahwa metode tersebut mengembalikan 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 memiliki API yang kaya dan berguna, yang dapat Anda baca lebih lanjut di artikel ini.

4. Penggusuran Nilai

Kafein memiliki tiga strategi untuk penggusuran nilai : berbasis ukuran, berbasis waktu, dan berbasis referensi.

4.1. Penggusuran Berbasis Ukuran

Jenis penggusuran ini mengasumsikan bahwa penggusuran terjadi saat batas ukuran cache yang dikonfigurasi terlampaui . Ada dua cara untuk mendapatkan ukuran - menghitung objek di cache, atau mendapatkan bobotnya.

Mari kita lihat bagaimana kita bisa menghitung objek di cache . Saat cache diinisialisasi, ukurannya sama dengan nol:

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

Saat kami menambahkan nilai, ukurannya jelas meningkat:

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

Kita dapat menambahkan nilai kedua ke cache, yang mengarah pada penghapusan nilai pertama:

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

Perlu disebutkan bahwa kami memanggil metode pembersihan sebelum mendapatkan ukuran cache . Ini karena penggusuran cache dijalankan secara asinkron, dan metode ini membantu menunggu selesainya penggusuran .

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

Pada artikel ini, kami berkenalan dengan pustaka cache Kafein untuk Java. Kami melihat cara mengonfigurasi dan mengisi cache, serta cara memilih kebijakan kedaluwarsa atau penyegaran yang sesuai sesuai kebutuhan kami.

Kode sumber yang ditampilkan di sini tersedia di Github.