Redisのキーの有効期限戦略と判断の原則
キーの有効期限を設定する
キーを設定するときに有効期限を設定できます。構文はSET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
次のとおりです。有効期限の2つのオプションは次のとおりです。
- EX秒:キーの有効期限を秒単位で設定します。
- PXミリ秒:キーの有効期限をミリ秒で設定します。
127.0.0.1:6379> set name morris ex 5
OK
有効期限コマンドを使用して、キーの有効期限、構文EXPIRE key seconds
:: を個別に設定できます。
127.0.0.1:6379> expire name 5
(integer) 1
ttlを使用してキーの有効期限を秒単位で表示し、pttlを使用してキーの有効期限をミリ秒単位で表示できます。
127.0.0.1:6379> set name morris ex 10
OK
127.0.0.1:6379> ttl name
(integer) 8
127.0.0.1:6379> pttl name
(integer) 5353
127.0.0.1:6379> ttl name
(integer) -2
127.0.0.1:6379> set name morris
OK
127.0.0.1:6379> ttl name
(integer) -1
期限切れの鍵または存在しない鍵の場合、ttlは-2を返します。通常の鍵の場合、ttlは-1を返します。有効期限が設定されている鍵の場合、ttlは期限切れまでの残り秒数を返します。
persistコマンドを使用して、タイムアウト制限を削除し、永続的なキーにすることができます。
127.0.0.1:6379> set name morris ex 10
OK
127.0.0.1:6379> persist name
(integer) 1
127.0.0.1:6379> ttl name
(integer) -1
注意点:
- キーが存在する場合、setコマンドを使用すると有効期限が上書きされ、新しいキーとして設定されます。
- renameコマンドでキーを変更すると、タイムアウト期間が新しいキーに転送されます。
127.0.0.1:6379> set name morris ex 30
OK
127.0.0.1:6379> set name tom
OK
127.0.0.1:6379> ttl name
(integer) -1
127.0.0.1:6379> set name morris ex 30
OK
127.0.0.1:6379> rename name myname
OK
127.0.0.1:6379> ttl myname
(integer) 22
期限切れのキーを削除する方法
Redisでキーの有効期限が切れた後、キーを削除するには、パッシブとアクティブの2つの方法があります。
アクティブな方法
キーが有効期限を超えた場合、キーはすぐに削除されません。期限切れのキーに対してdel、set、getsetが実行された場合にのみクリアされます。つまり、キーの値を変更するすべての操作が削除アクションをトリガーします。クライアントがアクセスしようとすると、キーが検出され、アクティブに期限切れになります。
受動的な方法
アクティブな方法では不十分です。期限切れの鍵にはアクセスできないため、期限切れになることはありません。Redisはパッシブな方法を提供しています。パッシブな方法は、期限切れの鍵を定期的に検出して削除します。具体的な操作は次のとおりです。
- 有効期限の検出のために100ミリ秒ごとに20個のキーをランダムに抽出します。
- 20個のキーのうち、期限切れのキーをすべて削除します。
- 期限切れのキーの割合が25%を超える場合は、手順1を繰り返します。
パッシブ方式では、確率アルゴリズムを使用してキーをランダムにサンプリングします。つまり、常に、期限切れのキーの最大1/4がクリアされます。
最大メモリを構成する
あなたはできる/etc/redis/6379.conf
のRedisのメモリサイズを制限するには、設定ファイル:
maxmemory <bytes>
maxmemoryを0に設定すると、メモリ制限がなくなります。
リサイクル戦略
指定されたメモリ制限サイズに達した場合、別の動作、つまりデータを追加するときにメモリ制限を回避できるように古いデータを取り戻す戦略を選択する必要があります。
maxmemory-policy
特定のリサイクル戦略はパラメータを介して構成でき、サポートされているリサイクル戦略は次のとおりです。
- noeviction:リサイクルしないでください。書き込みコマンドに直接エラーを返したり、操作を読み取ったりしないでください。データベースとしてredisを使用する場合は、このリサイクル戦略を使用する必要があります。この戦略はデフォルトで使用されます。
- Volatile-lru:有効期限付きのキーの中で、最も使用頻度の低いキーを再利用してみます。
- allkeys-lru:すべてのキーの中で、最も長く使用されていないキーを回復するようにしてください。
- Volatile-lfu:有効期限付きのキーの中で、最も使用頻度の低いキーを回復するようにしてください。
- allkeys-lfu:すべてのキーの中で、最も使用頻度の低いキーを回復するようにしてください。
- allkeys-random:すべてのキー間のランダムな回復。
- volatile-random:有効期限でランダムにキーを再利用します。
- volatile-ttl:有効期限付きのキーの中で、存続可能時間(TTL)が最も短いキーが優先されます。
リサイクルの前提条件が満たされていない場合、volatile-lru、volatile-random、およびvolatile-ttlの戦略はnoevictionに似ています。
近似LRUアルゴリズム
RedisのLRUアルゴリズムは完全な実装ではありません。つまり、実際のLRUアルゴリズムを使用するにはすべてのキーをスキャンする必要があるため、Redisと同様に、Redisはすべてのキーをスキャンする必要があるため、リサイクルのために最も長くアクセスされていないキーを選択できません。高性能設計は本来の意図に反します。
逆に、RedisはLRUと同様のアルゴリズムを使用して、少数のキーをサンプリングし、リサイクルのために最も長くアクセスされていないキーを選択します。Redisは、調整アルゴリズムの精度を達成するために収集されるたびにチェックされるサンプルの数を調整するために、次のパラメーターを提供します。
maxmemory-samples 5
アルゴリズムの実装
LRU
LRU(最も最近使用されていない、最も最近使用されていないアルゴリズム):データの一部が最近の期間にアクセスされなかった場合、それは将来アクセスされる可能性が低いと見なすことができます。したがって、領域がいっぱいになると、最も長い時間アクセスされていないデータが最初に削除されます。
実現:二重リンクリスト(LinkedList)+ハッシュテーブル(HashMap)で実現できます(リンクリストは場所を示すために使用され、ハッシュテーブルは格納と検索に使用されます)。
package com.morris.redis.demo.cache;
import java.util.HashMap;
import java.util.LinkedList;
public class LRUCache<K, V> {
private int capacity;
private int size;
private LinkedList<K> linkedList = new LinkedList<>();
private HashMap<K, V> hashMap = new HashMap();
public LRUCache(int capacity) {
this.capacity = capacity;
}
public void set(K k, V v) {
if(hashMap.containsKey(k)) {
// key存在
linkedList.remove(k); // 从双向链表中移除
linkedList.addFirst(k); // 插入到双向链表尾部
hashMap.put(k, v);
return;
}
// key不存在
if(size == capacity) {
linkedList.removeLast();
size--;
}
linkedList.addFirst(k);
hashMap.put(k, v);
size++;
}
public V get(K k) {
if(hashMap.containsKey(k)) {
linkedList.remove(k); // 从双向链表中移除
linkedList.addFirst(k); // 插入到双向链表尾部
return hashMap.get(k);
}
return null;
}
}
LinkedHashMapをJavaで直接使用して、以下を実現できます。
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache2<K, V> extends LinkedHashMap<K, V> {
private int capacity;
public LRUCache2(int capacity) {
super(16, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > capacity;
}
}
LFU
LFU(最も頻繁に使用されない、最も使用頻度の低いアルゴリズム):データの一部が最近の期間にほとんどアクセスされない場合、そのデータは将来アクセスされる可能性が低いと見なすことができます。したがって、スペースがいっぱいになると、使用頻度が最も低いデータが最初に削除されます。
package com.morris.redis.demo.cache.lfu;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
public class LFUCache<K, V> {
private int capacity;
private int size;
public LFUCache(int capacity) {
this.capacity = capacity;
}
private HashMap<K, Node<K, V>> hashMap = new HashMap<>();
private LinkedList<Node<K, V>> linkedList = new LinkedList<>();
public void set(K k, V v) {
if(hashMap.containsKey(k)) {
// 存在则更新
Node<K, V> node = hashMap.get(k);
node.count = 0; // 增加使用次数
node.lastTime = System.nanoTime(); // 更新使用时间
return;
}
// 不存在
if(size == capacity) {
// 删除最近最不常用的key
Collections.sort(linkedList, (k1, k2) -> {
// 先比较使用次数
if(k1.count > k2.count) {
return 1;
}
if(k1.count < k2.count) {
return -1;
}
// 再比较最后一次使用时间
if(k1.lastTime > k2.lastTime) {
return 1;
}
if(k1.lastTime < k2.lastTime) {
return -1;
}
return 0;
});
linkedList.removeFirst();
size--;
}
Node<K, V> node = new Node<>(k, v, System.nanoTime());
hashMap.put(k, node);
linkedList.addLast(node);
this.size++;
}
public V get(K k) {
V v = null;
if(hashMap.containsKey(k)) {
Node<K, V> node = hashMap.get(k);
node.count++; // 增加使用次数
node.lastTime = System.nanoTime(); // 更新使用时间
v = node.v;
}
return v;
}
public void print() {
System.out.println(linkedList);
}
private static class Node<K, V> {
K k;
V v;
int count; // 使用次数
long lastTime; // 最后一次使用时间
public Node(K k, V v, long lastTime) {
this.k = k;
this.v = v;
this.lastTime = lastTime;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Node{");
sb.append("k=").append(k);
sb.append(", count=").append(count);
sb.append(", lastTime=").append(lastTime);
sb.append('}');
return sb.toString();
}
}
}
LinkedListを使用するには、すべてのキーを完全にソートする必要があり、時間の複雑さはO(n)です。
LFUが最も頻繁にアクセスされないデータを排除することを考えると、データアクセスの頻度を桁違いに維持する適切な方法が必要です。LFUアルゴリズムは、本質的にトップK問題(K = 1)と見なすことができます。つまり、最小の周波数を持つ要素が選択されます。したがって、バイナリヒープを使用して最小の周波数を持つ要素を選択することは簡単に考えることができます。この実装はより効率的です。実装戦略は、小さなトップヒープ+ハッシュテーブルです。
バイナリヒープを使用してすべてのキーの中で最小のものを見つけると、時間の複雑さはO(logn)になります。
package com.morris.redis.demo.cache.lfu;
import java.util.HashMap;
import java.util.PriorityQueue;
public class LFUCache2<K, V> {
private int capacity;
private int size;
public LFUCache2(int capacity) {
this.capacity = capacity;
}
private HashMap<K, Node<K, V>> hashMap = new HashMap<>();
private PriorityQueue<Node<K, V>> priorityQueue = new PriorityQueue<>((k1, k2) -> {
// 先比较使用次数
if(k1.count > k2.count) {
return 1;
}
if(k1.count < k2.count) {
return -1;
}
// 再比较最后一次使用时间
if(k1.lastTime > k2.lastTime) {
return 1;
}
if(k1.lastTime < k2.lastTime) {
return -1;
}
return 0;
});
public void set(K k, V v) {
if(hashMap.containsKey(k)) {
// 存在则更新
Node<K, V> node = hashMap.get(k);
node.count = 0; // 增加使用次数
node.lastTime = System.nanoTime(); // 更新使用时间
return;
}
// 不存在
if(size == capacity) {
// 删除最近最不常用的key
priorityQueue.remove();
size--;
}
Node<K, V> node = new Node<>(k, v, System.nanoTime());
hashMap.put(k, node);
priorityQueue.add(node);
this.size++;
}
public V get(K k) {
V v = null;
if(hashMap.containsKey(k)) {
Node<K, V> node = hashMap.get(k);
node.count++; // 增加使用次数
node.lastTime = System.nanoTime(); // 更新使用时间
v = node.v;
}
return v;
}
public void print() {
System.out.println(priorityQueue);
}
private static class Node<K, V> {
K k;
V v;
int count; // 使用次数
long lastTime; // 最后一次使用时间
public Node(K k, V v, long lastTime) {
this.k = k;
this.v = v;
this.lastTime = lastTime;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Node{");
sb.append("k=").append(k);
sb.append(", count=").append(count);
sb.append(", lastTime=").append(lastTime);
sb.append('}');
return sb.toString();
}
}
}