序文
Javaは最初からredisを手動で実装します(1)固定サイズのキャッシュを実現するにはどうすればよいですか?
Javaはredisを最初から手作業で実装します(3)redisの有効期限の原則
Javaは最初から手動でredisを実装します(3)メモリデータを失うことなく再起動する方法は?
Javaは手動でredisを最初から実装します(4つ)リスナーを追加します
Javaは最初から手動でredisを実装します(6)AOF永続性の原則の詳細と実装
redisのいくつかの機能を実装しただけです。Javaはredisを最初から手動で実装します(1)固定サイズのキャッシュを実現するにはどうすればよいですか?ファーストイン、ファーストアウトの排除戦略が中国で実施されました。
ただし、実際の作業では、通常、LRU / LFU除去戦略を使用することをお勧めします。
LRUの基本
学習を拡大する
Apache CommonsLRUMAPソースコードの詳細な説明
LRUとは
LRUは、最も最近使用されていないことを意味する「最近使用されていない」の最初の文字で構成され、オブジェクト除去アルゴリズムで一般的に使用されます。
これは、比較的一般的な除去アルゴリズムでもあります。
核となる考え方は、データが最近アクセスされた場合、将来アクセスされる可能性も高くなるということです。
連続
コンピュータサイエンスには、継続性の原則という指針があります。
時間の継続性:情報訪問の場合、最近訪問された場合、再度訪問される可能性が非常に高くなります。キャッシュは、データを排除するためにこの概念に基づいています。
空間的連続性:ディスク情報にアクセスするために、連続的な空間情報にアクセスすることは非常に可能です。そのため、パフォーマンスを向上させるためにページのプリフェッチが行われます。
実装手順
-
リンクリストの先頭に新しいデータが挿入されます。
-
キャッシュがヒットする(つまり、キャッシュされたデータにアクセスする)たびに、データをリンクリストの先頭に移動します。
- リンクリストがいっぱいになると、リンクリストの最後のデータは破棄されます。
実際、それは比較的単純です。FIFOキューと比較して、リンクされたリストを導入できます。
少し考えた
上記の3つの文を1つずつ検討して、最適化に値するポイントやピットがあるかどうかを確認してみましょう。
それが新しいデータであると判断する方法は?
(1)リンクリストの先頭に新しいデータを挿入します。
リンクリストを使用しています。
新しいデータが存在するかどうかを判断する最も簡単な方法は、トラバースすることです。リンクされたリストの場合、これはO(n)時間の複雑さです。
実際、パフォーマンスはまだ比較的劣っています。
もちろん、セットを紹介するなど、時間のある空間を考えることもできますが、これは空間へのプレッシャーを倍増させます。
キャッシュヒットとは
(2)キャッシュがヒットする(つまり、キャッシュされたデータにアクセスする)たびに、データをリンクリストの先頭に移動します。
put(key、value)の場合、それは新しい要素です。この要素がすでに存在する場合は、上記の処理を参照して、最初に削除してから追加することができます。
get(key)の場合、要素アクセスの場合、既存の要素が削除され、新しい要素がヘッドに配置されます。
remove(key)要素を削除し、既存の要素を直接削除します。
keySet()valueSet()entrySet()これらは無差別アクセスであり、キューを調整しません。
削除する
(3)リンクリストがいっぱいになったら、リンクリストの最後にあるデータを破棄します。
リンクリストがいっぱいの場合、つまり要素を追加する場合、つまりput(key、value)を実行する場合のシナリオは、1つだけです。
対応するキーを直接削除するだけです。
javaコードの実装
インターフェイスの定義
これはFIFOインターフェースと一貫性があり、呼び出し場所も変更されていません。
その後のLRU / LFUの実装のために、2つの新しいメソッドremove / updateが追加されました。
public interface ICacheEvict<K, V> {
/**
* 驱除策略
*
* @param context 上下文
* @since 0.0.2
* @return 是否执行驱除
*/
boolean evict(final ICacheEvictContext<K, V> context);
/**
* 更新 key 信息
* @param key key
* @since 0.0.11
*/
void update(final K key);
/**
* 删除 key 信息
* @param key key
* @since 0.0.11
*/
void remove(final K key);
}
LRUの実装
LinkedListに基づいて直接実現します。
/**
* 丢弃策略-LRU 最近最少使用
* @author binbin.hou
* @since 0.0.11
*/
public class CacheEvictLRU<K,V> implements ICacheEvict<K,V> {
private static final Log log = LogFactory.getLog(CacheEvictLRU.class);
/**
* list 信息
* @since 0.0.11
*/
private final List<K> list = new LinkedList<>();
@Override
public boolean evict(ICacheEvictContext<K, V> context) {
boolean result = false;
final ICache<K,V> cache = context.cache();
// 超过限制,移除队尾的元素
if(cache.size() >= context.size()) {
K evictKey = list.get(list.size()-1);
// 移除对应的元素
cache.remove(evictKey);
result = true;
}
return result;
}
/**
* 放入元素
* (1)删除已经存在的
* (2)新元素放到元素头部
*
* @param key 元素
* @since 0.0.11
*/
@Override
public void update(final K key) {
this.list.remove(key);
this.list.add(0, key);
}
/**
* 移除元素
* @param key 元素
* @since 0.0.11
*/
@Override
public void remove(final K key) {
this.list.remove(key);
}
}
実装は比較的単純で、FIFOよりも3つの方法があります。
update():少し単純化して、アクセスである限り削除されてから、キューの先頭に挿入されると考えます。
remove():削除することは直接削除することです。
これらの3つの方法は、最近の使用状況を更新するために使用されます。
いつ呼ばれますか?
注釈属性
コアプロセスを確実にするために、アノテーションに基づいて実装します。
属性を追加します。
/**
* 是否执行驱除更新
*
* 主要用于 LRU/LFU 等驱除策略
* @return 是否
* @since 0.0.11
*/
boolean evict() default false;
注釈の使用
どのような方法を使用する必要がありますか?
@Override
@CacheInterceptor(refresh = true, evict = true)
public boolean containsKey(Object key) {
return map.containsKey(key);
}
@Override
@CacheInterceptor(evict = true)
@SuppressWarnings("unchecked")
public V get(Object key) {
//1. 刷新所有过期信息
K genericKey = (K) key;
this.expire.refreshExpire(Collections.singletonList(genericKey));
return map.get(key);
}
@Override
@CacheInterceptor(aof = true, evict = true)
public V put(K key, V value) {
//...
}
@Override
@CacheInterceptor(aof = true, evict = true)
public V remove(Object key) {
return map.remove(key);
}
注釈除外インターセプターの実装
実行順序:メソッドの後に更新します。それ以外の場合は、現在の各操作のキーが最初に配置されます。
/**
* 驱除策略拦截器
*
* @author binbin.hou
* @since 0.0.11
*/
public class CacheInterceptorEvict<K,V> implements ICacheInterceptor<K, V> {
private static final Log log = LogFactory.getLog(CacheInterceptorEvict.class);
@Override
public void before(ICacheInterceptorContext<K,V> context) {
}
@Override
@SuppressWarnings("all")
public void after(ICacheInterceptorContext<K,V> context) {
ICacheEvict<K,V> evict = context.cache().evict();
Method method = context.method();
final K key = (K) context.params()[0];
if("remove".equals(method.getName())) {
evict.remove(key);
} else {
evict.update(key);
}
}
}
removeメソッドについては特別な判断のみを行い、他のすべてのメソッドはupdateを使用して情報を更新します。
パラメータは最初のパラメータを直接取ります。
テスト
ICache<String, String> cache = CacheBs.<String,String>newInstance()
.size(3)
.evict(CacheEvicts.<String, String>lru())
.build();
cache.put("A", "hello");
cache.put("B", "world");
cache.put("C", "FIFO");
// 访问一次A
cache.get("A");
cache.put("D", "LRU");
Assert.assertEquals(3, cache.size());
System.out.println(cache.keySet());
- ログ情報
[D, A, C]
また、BがremoveListenerログから削除されていることも確認できます。
[DEBUG] [2020-10-02 21:33:44.578] [main] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: B, value: world, type: evict
概要
redis LRU除去戦略は、実際には実際のLRUではありません。
LRUには大きな問題があります。つまり、O(n)が検索するたびに、キーの数が特に多い場合、これは依然として非常に低速です。
redisがこのように設計されている場合、それは間違いなく遅くなります。
Map<String, Integer>
インデックスとリストに格納されているキーを追加するなど、時間のスペースO(1)を使用して速度を見つけることができますが、空間の複雑さは2倍になります。
しかし、犠牲はそれだけの価値があります。この種のフォローアップ統合最適化では、さまざまな最適化ポイントが考慮されるため、全体的な状況を調整でき、後の統合調整にも便利です。
次のセクションでは、次の改善されたバージョンのLRUを一緒に実装します。
Redisが行うことは、一見単純なことで究極を達成することです。これは、すべてのオープンソースソフトウェアで学ぶ価値があります。
記事は主にアイデアについて述べていますが、スペースの制限のため、実現部分はすべて掲載されていません。
オープンソースアドレス:https://github.com/houbb/cache
この記事がお役に立てば幸いです。いいね、コメント、ブックマーク、そして波をフォローしてください〜
あなたの励ましが私の最大の動機です〜