1. カフェインの概要
1. キャッシュの導入
キャッシュ (Cache) はコードの世界では遍在しています。基盤となる CPU のマルチレベル キャッシュからクライアントのページ キャッシュに至るまで、あらゆる場所にキャッシュがあります。キャッシュとは本来、空間と時間を交換する手段であり、データを一定の空間に配置することで、次のデータアクセスを高速化することができます。
Java に関する限り、データベース キャッシング フレームワーク EhCache、分散キャッシング Memcached など、一般的に使用されるキャッシング ソリューションが多数あります。これらのキャッシング ソリューションは、実際には、スループット効率を向上させ、永続層への過剰な負荷を回避するように設計されています。
一般的なキャッシュの種類は、ローカル キャッシュと分散キャッシュに分けられ、Caffeine は優れたローカル キャッシュ、Redis は分散キャッシュとして使用できます。
2. カフェインの概要
公式カフェイン:
https://github.com/ben-manes/caffeine
Caffeine は Java 1.8 ベースの高性能ローカル キャッシュ ライブラリで、Guava によって改良されており、Spring 5 以降のデフォルトのキャッシュ実装ではオリジナルの Google Guava が Caffeine に置き換えられており、そのキャッシュ ヒット率が最適値に近いことが公式説明で指摘されています。価値。実際、Caffeine のようなローカル キャッシュは ConcurrentMap に非常に似ています。つまり、同時実行性と O(1) の時間計算量でのデータ アクセスをサポートします。2 つの主な違いは次のとおりです。
- ConcurrentMap は、明示的に削除するまで、保存されているすべてのデータを保存します。
- Caffeine は、メモリの占有を合理的に維持するために、指定された構成を通じて「使用頻度の低い」データを自動的に削除します。
したがって、これを理解するためのより良い方法は、「キャッシュは、ストレージと削除の戦略を備えたマップである」ということです。
2. カフェインの基礎
Caffeine を使用するには、プロジェクトに次の依存関係を導入する必要があります。
<依存関係><groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>カフェイン</artifactId>
;<!--https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeinez找最新版-->
<バージョン>3.0.5</バージョン>
</依存関係>
1. キャッシュ読み込み戦略
1.1 キャッシュマニュアル作成
最も一般的なタイプのキャッシュでは、読み込みメソッドを指定する必要はなく、読み込みのために put() を手動で呼び出す必要があります。put() メソッドは既存のキーを上書きすることに注意してください。これは Map のパフォーマンスと一致します。キャッシュされた値を取得するときに、キャッシュされた値が存在しないときにその値をアトミックにキャッシュに書き込みたい場合は、 get(key, k -> value) メソッドを呼び出すことができます。これにより、書き込み競合が回避されます。validate() メソッドを呼び出すと、キャッシュが手動で削除されます。
マルチスレッドの場合、get(key, k -> value) を使用するときに、別のスレッドが同時に競合するためにこのメソッドを呼び出した場合、前のスレッドがキャッシュを更新するまで、後のスレッドはブロックされます。スレッドが getIfPresent() メソッドを呼び出した場合、ブロックされることなく直ちに null を返します。
Cache<Object, Object> cache = Caffeine.newBuilder()//初期数
; .initialCapacity(10)
//最大条数
; 。
MaximumSize(10) //expireAfterWrite とexpireAfterAccess が同時に存在する場合、expireAfterWrite が優先されます &
nbsp ; //最後の書き込み操作後に指定された時間が期限切れになります
.expireAfterWrite(1, TimeUnit .秒)
RecordStats() .build(); cache.put( "1", "张三"); //张三
System.out.println(cache.getIfPresent( "1"));
//デフォルト値を保存します
System.out.println(cache.get( "2",o ; -> "デフォルト値"));
1.2 読み込みキャッシュの自動作成
LoadingCache は自動ロードされるキャッシュです。通常のキャッシュとは異なり、キャッシュが存在しない場合、またはキャッシュの有効期限が切れた場合、 get() メソッドが呼び出されると、自動的に CacheLoader.load() メソッドが呼び出され、最新の値がロードされます。CacheLoader.loadAll() メソッドが実装されていない限り、getAll() メソッドを呼び出すと、すべてのキーが走査されて get() が呼び出されます。LoadingCache を使用する場合は、CacheLoader を指定し、キャッシュが見つからない場合に自動的にロードできるように、その中にload() メソッドを実装する必要があります。
マルチスレッドの場合、2 つのスレッドが同時に get() を呼び出すと、前のスレッドがキャッシュを更新するまで、後のスレッドはブロックされます。
LoadingCache<String, String> loadingCache = Caffeine.newBuilder()//キャッシュが作成されてから指定された時間が経過したか、最新のキャッシュ更新 , キャッシュを更新します。refreshAfterWrite は LoadingCache のみをサポートします ;
&
nbsp ;.expireAfterWrite(10, TimeUnit.SECONDS)
.expireAfterAccess(10, ) ;TimeUnit.SECONDS) ;
; 。
MaximumSize(10) //キーに従ってデータベース内の値をクエリします。これはランバ式です ; &
nbsp ;.build(key -> new Date().toString());
1.3 Async Cacheの非同期取得
AsyncCache は Cache のバリアントであり、その応答結果は CompletableFuture であり、このようにして AsyncCache は非同期プログラミング モデルに適応します。デフォルトでは、キャッシュ計算ではスレッド プールとして ForkJoinPool.commonPool() が使用されます。スレッド プールを指定したい場合は、Caffeine.executor(Executor) メソッドをオーバーライドして実装できます。synchronous() は、非同期キャッシュが生成され、キャッシュとして返されるまでブロックする機能を提供します。
マルチスレッドの場合、2 つのスレッドが同時に get(key, k -> value) を呼び出すと、同じ CompletableFuture オブジェクトが返されます。返される結果自体はブロックしないため、業務設計に応じてブロック待ちかノンブロッキングかを選択できます。
AsyncLoadingCache<String, String> asyncLoadingCache = Caffeine.newBuilder()//キャッシュが作成されてから、指定された時間間隔が経過しました。作成されたキャッシュまたは最新のキャッシュ更新 キャッシュを更新します。LoadingCache のみをサポートします
.refreshAfterWrite(1, TimeUnit.SECONDS) ;
;.expireAfterWrite(1, TimeUnit.SECONDS)
.expireAfterAccess(1, TimeUnit .SECONDS) &
nbsp ; 。
MaximumSize(10 ) //キーに従ってデータベース内の値をクエリします .buildAsync
(キー -> {
Thread.sleep(1000);
return new Date().toString();
});
// 返されるのは CompletableFuture
CompletableFuture<String> future = asyncLoadingCache.get( "1");
future.thenAccept(System.out::println);
2. 追放戦略
エビクション ポリシーは、キャッシュの作成時に指定されます。一般的に使用されるのは、容量ベースのエビクションと時間ベースのエビクションです。
容量ベースのエビクションでは、キャッシュ容量の最大値を指定する必要があります。キャッシュ容量が最大値に達すると、Caffeine は LRU 戦略を使用してキャッシュを削除します。時間ベースのエビクション ポリシーは、次の時点でキャッシュにアクセス/書き込みするように設定できます。指定された時間の終了後、自動的に消去されます。
エビクション戦略は組み合わせて使用でき、いずれかのエビクション戦略が有効になると、キャッシュ エントリがエビクションされます。
- LRU は最も最近使用されていないページであり、最も長期間使用されていないページは削除されます。
- LFU 最も使用頻度の低いページは、一定期間にわたって最も使用頻度の低いページを削除します。
- FIFO 先入れ先出し
Caffeineには4つのキャッシュ削除設定があります
- サイズ (消去用の LFU アルゴリズム)
- 重量(サイズと重量のどちらか一方のみ選択可能)
- 時間
- 引用 (一般的には使用されないため、この記事では取り上げません)
public class CacheTest {
/**
* 缓存大小淘汰
*/
@Test
public void maximumSizeTest() throws InterruptedException { &nb sp;キャッシュ<整数
、 整数> キャッシュ = カフェイン。
newBuilder() //超过10个次会使用W-TinyLFU算法进行淘汰&
nbsp ; .maximumSize(10)
val); }) .build(); for (int i = 1; i < 20; i++) { 建てる(); for (int i = 1; i < 20; i++) { 建てる(); for (int i = 1; i < 20; i++) {
cache.put(i, i);
}
Thread.sleep(500);//缓存淘汰は异步的 &
nbsp ; // 打印还不被淘汰の保存&
nbsp; System.out.println(cache. asMap());
} &
nbsp; /**
* 权重淘汰 *
/ &
nbsp; ; @Test
public void maximumWeightTest() throws InterruptedException {
Cache<Integer, Integer> cache = Caffeine.newBuilder()
; //すべてのキャッシュの重みが合計される場合、合計の重みを制限します > 合計の重みにより、重みの小さいキャッシュが削除されます & nbsp ;&
nbsp; .maximumWeight(100) &nb sp
; .weigher((Weigher<Integer, Integer>) (key,
value) -> key) .evictionListener((key, val, removalCause) -> {
log.info( "淘汰缓存:key: {} val:{}", key, val);
})
。 建てる();
//总权重其实是=すべての保存权重加起 int maximumWeight ;
= 0;
for (int i = 1; i < 20; i++) {
cache.put(i, i);
maximumWeight += i;
}
System.out.println( "总权重=" + maximumWeight );
スレッド。
*/
@Test
public void expireAfterAccessTest() throws InterruptedException { &nb sp;
; Cache<Integer, Integer> cache = Caffeine.newBuilder()
; .expireAfterAccess(1, TimeUnit.
SECONDS) //期限切れのキャッシュ アイテムを適時に削除するようにスケジューラを指定できますカフェインは待つ代わりに定期的なメンテナンスをトリガーします
// スケジューラが設定されていない場合、キャッシュ次回 get を呼び出します。受動的に削除されます
val); }) .build(); cache.put(1, 2);
System.out.println(cache.getIfPresent(1));
Thread.sleep(3000);
System.out.println(キャッシュ.
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; nbsp; nbsp; nbsp; nbsp; nbsp; p ;&nb
sp ; //Caffeine がトリガーされるのを待つ代わりに、期限切れのキャッシュ項目を時間内に削除するようにスケジューラを指定できます定期メンテナンス & nbsp
; //スケジューラが設定されていない場合、キャッシュは次のときに受動的に削除されます。 get &
nbsp ; .scheduler(Scheduler.
systemScheduler()) .evictionListener((key, val, removalCause) -> {
log.info( "淘汰缓存:key: {} val:{}", key, val);
})
.build();
cache.put(1, 2);
スレッド。 スリープ(3000);
System.out.println(cache.getIfPresent(1));//null
}
}
3. リフレッシュ機構
freshAfterWrite() は、x 秒後にキャッシュを自動的にリフレッシュする戦略を削除戦略と組み合わせて使用できることを示します。リフレッシュ メカニズムは LoadingCache と AsyncLoadingCache のみをサポートしていることに注意してください
private static int NUM = 0;@Test
public void refreshAfterWriteTest() throws InterruptedException {
LoadingCache<Integer, Integer> cache = Caffeine.newBuilder() &n bsp;
; .refreshAfterWrite(1, TimeUnit.SECONDS) &
nbsp ; //模拟获取データベース、次回获取から增1
。
// 2 秒の遅延の後、理論的には、キャッシュを自動的に更新した後に得られる値は 2 になります。 // しかし、そうではなく、値は1 のままです。
これは、refreshAfterWrite が n 秒後の再取得後に自動的に更新するように設定されていないためです
// が、x 秒後に && getIfPresent が 2 回目に呼び出されたときに受動的に更新されます &
nbsp ; ;Thread.sleep(2000);
System.out.println(cache.getIfPresent(1));// 1
//それこの時点でキャッシュが更新されますが、初回は古い値が引き続き取得されます
System.out.println(cache.getIfPresent(1));// 2
}
4. 統計
LoadingCache<String, String> cache = Caffeine.newBuilder()//キャッシュが作成されてから指定された時間が経過したか、または最新のキャッシュ更新
、キャッシュを更新します。refreshAfterWrite は LoadingCache
;.expireAfterWrite(1, TimeUnit.SECONDS)
.expireAfterAccess(1, TimeUnit) .SECONDS) &
nbsp ; 。
MaximumSize(10 ) //レコード キャッシュ ヒット率などの情報を有効にする .recordStats()
&
nbsp ; //キーに従ってデータベース内の値をクエリします
.build(key -> {
; Thread.sleep(1000);
return new Date().toString(); &
nbsp ; });
cache.put( "1", "ショーン");
キャッシュ.get( "1");
/*
* hitCount :命中の次数
* missCount:未命中の次数
* requestCount:
* totalLoadCount: アイテムの合計数
* loadExceptionRate: 新しい値のロード失敗率
;* totalLoadTime: 合計ロード時間
* evictionCount: 欠落しているアイテムの数
;*/
System.out.println(cache.stats());
5. まとめ
上記の戦略の中には作成時に自由に組み合わせることができるものもありますが、一般的には 2 つの方法があります。
- maxSize、refreshAfterWrite を設定し、expireAfterWrite/expireAfterAccess を設定せず、expireAfterWrite を設定します。キャッシュの有効期限が切れると、ロックしてキャッシュを同期的に取得します。そのため、expireAfterWrite を設定するとパフォーマンスは向上しますが、古いデータがフェッチされる場合があります。古いデータのフェッチが許可されているシナリオに適しています
- maxSize、expireAfterWrite/expireAfterAccessを設定し、refreshAfterWriteを設定しないでください。データの整合性は良好で、古いデータは取得されませんが、パフォーマンスは(比較して)それほど良くありません。データ取得に時間がかからないシナリオに適しています。 -消費する
3. SpringBoot は Caffeine を統合します
1. @Cacheable についてのコメント
1.1 関連する依存関係
@Cacheable アノテーションを使用する場合は、関連する依存関係を導入し、@EnableCaching アノテーションを構成クラス ファイルに追加する必要があります。
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
1.2 一般的なアノテーション
- @Cacheable : メソッドがキャッシュをサポートしていることを示します。注釈付きメソッドが呼び出されたときに、対応するキーがキャッシュにすでに存在する場合、メソッド本体は実行されず、キャッシュから直接返されます。メソッドが null を返す場合、キャッシュ操作は実行されません。
- @CachePut : メソッドの実行後、その値が最新の結果としてキャッシュに更新され、メソッドが毎回実行されることを示します。
- @CacheEvict : メソッドの実行後にキャッシュのクリア操作がトリガーされることを示します。
- @Caching : 最初の 3 つのアノテーションを結合するために使用されます。例:
evict = {@CacheEvict( "CacheConstants.GET_DYNAMIC" ,allEntries = true )}
public User find(Integer id) {
return null;
}
1.3 共通のアノテーション属性
- cacheNames/value : キャッシュ コンポーネントの名前、つまり、cacheManager 内のキャッシュの名前。
- key : データをキャッシュするときに使用されるキー。デフォルトではメソッドのパラメータ値が使用されますが、SpEL 式を使用して記述することもできます。
- keyGenerator : 2 つのキーのいずれかを使用します。
- cacheManager : 使用するキャッシュ マネージャーを指定します。
- 条件: メソッドの実行開始前にチェックし、条件が満たされた場合にキャッシュします。
- until : メソッドの実行後にチェックし、一致する場合はキャッシュしません。
- sync : 同期モードを使用するかどうか。同期モードが使用されている場合、複数のスレッドが同時にキーをロードすると、他のスレッドがブロックされます。
1.4 キャッシュ同期モード
同期がオンまたはオフになっており、Cache と LoadingCache のパフォーマンスに一貫性がありません。
- キャッシュでは、sync はすべてのスレッドが同期的に待機する必要があるかどうかを示します。
- LoadingCache では、sync は、存在しない/削除されたキーを読み取るときにアノテーション付きメソッドを実行するかどうかを示します。
2.実戦
2.1 依存関係の導入
<dependency><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<依存関係>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>カフェイン</artifactId>
</dependency>
2.2 キャッシュ定数 CacheConstants
キャッシュ定数クラスを作成し、共通定数のレイヤーを抽出し、それらを再利用します。ここでは、@ConfigurationProperties や @Value などの構成ファイルを通じてこれらのデータをロードすることもできます
public&nbsp; class&nbsp; cacheconstants&nbsp; {&nbsp;&nbsp;&nbsp;&nbsp;/**
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;*&nbsp; ;3*60 は 3 分)
*/ &
nbsp; public static final int DEFAULT_EXPIRES = 3 * 60 ;
public static final int EXPIRES_5_MIN = 5 * 60;
public static final int EXPIRES_10_MIN = 10 * 60; <a i=7> public static final String GET_USER = "取得:ユーザー";
public static final String GET_DYNAMIC = "取得:動的";
}
2.3 キャッシュ構成クラス CacheConfig
@configuration@enablecaching
public&nbsp; class&nbsp; cacheconfig&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;/**
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ;
&nbsp ;* ;InitialCapacity=[integer]: 初期キャッシュ スペース サイズ
* maximumSize=[long]: キャッシュ アイテムの最大数
* maximumWeight=[long]: キャッシュの最大重み
* expireAfterAccess=[duration]: 最後の書き込みまたはアクセスから一定時間が経過すると期限切れになります
; * expireAfterWrite=[期間]:
最後の書き込み後に固定時間が経過します * refreshAfterWrite=[duration]: 一定の時間間隔後にキャッシュが作成または更新され、キャッシュを更新します
キーの弱い参照を開きます *weakValues: 値の弱い参照を開きます
* SoftValues: 値のソフト参照を開きます
; * RecordStats: 統計関数の開発
* 注:
* ExpireAfterWrite と ExpireAfterAccess が存在する場合、expireAfterWrite が優先されます。
* maximumSize と MaximumWeight は同時に使用できません
* weakValues と SoftValues は同時に使用できません
*/
@Bean <a i=10> public CacheManager キャッシュマネージャー() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> list = new ArrayList<>();
//循環追加枚举类中に自動的に保存されている、独立可能
; for (CacheEnum cacheEnum : CacheEnum.values()) {
; リスト。
&n bsp; .initialCapacity(50)
&注意; 。
最大サイズ(1000) ; .expireAfterAccess(cacheEnum.getExpires(), TimeUnit.SECONDS) &nb
sp ; .build()));秒) &nb sp; .build()));秒) &nb sp; .build()));
}
cacheManager.setCaches(list);
キャッシュマネージャーを返す;
}
}
2.4 コールキャッシュ
ここで注意すべき点は、@Transactional と同様に Cache もプロキシを使用するため、クラス内の呼び出しは無効になるということです。
/*** 値: キャッシュ キーのプレフィックス。
* key: キャッシュ キーのサフィックス。
* sync: キャッシュの有効期限が切れた場合に 1 つのリクエストだけをデータベースに送信し、他のリクエストをブロックするかどうかを設定します。デフォルトは false です(個人のニーズに応じて)。
* ただし、null 値をキャッシュしません。ここで使用しない場合、エラーが報告されます。
* ユーザー情報クラスをクエリ
します。 * カスタム文字列を追加する必要がある場合は、一重引用符を使用する必要があります。
* クエリが null の場合もキャッシュされます
*/
@Cacheable(value = CacheConstants.GET_USER,key = "'user'+#userId",sync = true ) @CacheEvict public UserEntity & nbsp
;
getUserByUserId (Integer userId){
UserEntity userEntity = userMapper.
ユーザーエンティティを返す;
}