Estratégia de expiração da chave Redis e princípio de julgamento
Defina o tempo de expiração da chave
Você pode definir o tempo de expiração ao definir uma chave, a sintaxe é :, SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
as duas opções para o tempo de expiração são as seguintes:
- Segundos EX: Defina o tempo de expiração da chave, em segundos.
- Milissegundos PX: Defina o tempo de expiração da chave em milissegundos.
127.0.0.1:6379> set name morris ex 5
OK
Você pode usar o comando expire para definir individualmente o tempo de expiração da chave, a sintaxe EXPIRE key seconds
::
127.0.0.1:6379> expire name 5
(integer) 1
Você pode usar ttl para ver o tempo de expiração da chave em segundos e pttl para ver o tempo de expiração da chave em milissegundos:
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
Para uma chave expirada ou inexistente, ttl retorna -2. Para uma chave normal, ttl retorna -1. Para uma chave com um tempo de expiração definido, ttl retorna o número de segundos restantes para expirar.
Você pode usar o comando persist para remover o limite de tempo limite e torná-lo uma chave permanente:
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
ponto importante:
- Se a chave existir, o uso do comando set sobrescreverá o tempo de expiração, o que significa que será definido como uma nova chave.
- Se a chave for modificada pelo comando renomear, o período de tempo limite será transferido para a nova chave.
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
Como eliminar chaves expiradas
Depois que a chave expira no Redis, há duas maneiras de eliminá-la: passiva e ativa.
Maneira ativa
Quando a chave excede o tempo de expiração, a chave não será excluída imediatamente. Ela será apagada apenas quando del, set, getset forem executados na chave expirada, o que significa que todas as operações para alterar o valor da chave irão acionar a ação de exclusão. Quando o cliente Ao tentar acessá-lo, a chave será descoberta e expirará ativamente.
Forma passiva
O método ativo não é suficiente. Como algumas chaves expiradas nunca serão acessadas, elas nunca expirarão. O Redis fornece um método passivo. O método passivo detecta periodicamente as chaves expiradas e as exclui. As operações específicas são as seguintes:
- Extraia aleatoriamente 20 chaves a cada 100 ms para detecção de expiração.
- Exclua todas as chaves expiradas entre as 20 chaves.
- Se a proporção de chaves expiradas for maior que 25%, repita a etapa 1.
O método passivo usa um algoritmo de probabilidade para amostrar chaves aleatoriamente, o que significa que a qualquer momento, até 1/4 das chaves expiradas serão apagadas.
Configurar memória máxima
Você pode /etc/redis/6379.conf
limitar o tamanho da memória do redis no arquivo de configuração:
maxmemory <bytes>
Definir maxmemory como 0 significa que não há limite de memória.
Estratégia de reciclagem
Quando o tamanho do limite de memória especificado é atingido, você precisa escolher um comportamento diferente, ou seja, uma estratégia para recuperar alguns dados antigos de forma que o limite de memória possa ser evitado ao adicionar dados.
A maxmemory-policy
estratégia de reciclagem específica pode ser configurada por meio de parâmetros, e as estratégias de reciclagem com suporte são as seguintes:
- noeviction: não reciclar, retornar um erro diretamente para o comando de gravação ou operações de leitura.Se você usar o redis como banco de dados, deverá usar esta estratégia de reciclagem, que é usada por padrão.
- Volatile-lru: Entre as chaves com tempo de expiração, tente recuperar a chave menos usada recentemente.
- allkeys-lru: Entre todas as chaves, tente recuperar a chave menos usada recentemente.
- Volatile-lfu: Entre as chaves com tempo de expiração, tente recuperar a chave menos usada.
- allkeys-lfu: Entre todas as chaves, tente recuperar a chave menos usada.
- allkeys-random: recuperação aleatória entre todas as chaves.
- volatile-random: recupera as chaves aleatoriamente com o tempo de expiração.
- volatile-ttl: Entre as chaves com tempo de expiração, a chave com o menor tempo de vida (TTL) é a preferida.
Se os pré-requisitos para reciclagem não forem atendidos, as estratégias volatile-lru, volatile-random e volatile-ttl serão semelhantes às de noeviction.
Algoritmo LRU aproximado
O algoritmo LRU no Redis não é uma implementação completa, o que significa que o Redis não pode selecionar a chave que não foi acessada por mais tempo para reciclagem, porque o uso do algoritmo LRU real requer a varredura de todas as chaves, o que levará muito tempo, o que é semelhante ao redis. O design de alto desempenho vai contra a intenção original.
Ao contrário, o Redis usa um algoritmo semelhante ao LRU, amostrando um pequeno número de chaves e, em seguida, selecionando as chaves que não foram acessadas por mais tempo para reciclagem. O Redis fornece os seguintes parâmetros para ajustar o número de amostras verificadas cada vez que é coletado para atingir a precisão do algoritmo de ajuste:
maxmemory-samples 5
Implementação de algoritmo
LRU
LRU (O algoritmo menos usado recentemente, o algoritmo menos usado recentemente): se um dado não foi acessado no período de tempo mais recente, pode-se considerar que é improvável que ele seja acessado no futuro. Portanto, quando o espaço está cheio, os dados que não foram acessados por mais tempo são eliminados primeiro.
Realização: Pode ser realizada com uma lista duplamente vinculada (LinkedList) + tabela hash (HashMap) (a lista vinculada é usada para indicar a localização, e a tabela hash é usada para armazenar e pesquisar).
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 pode ser usado diretamente em Java para alcançar:
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 (Least Frequently Used, o algoritmo menos frequentemente usado): Se um dado raramente é acessado no período recente, pode-se considerar que é improvável que ele seja acessado no futuro. Portanto, quando o espaço está cheio, os dados usados com menos frequência são eliminados primeiro.
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();
}
}
}
Usar LinkedList requer que todas as chaves sejam classificadas completamente, e a complexidade de tempo é O (n).
Considerando que o LFU eliminará os dados acessados com menos frequência, precisamos de um método adequado para manter a frequência de acesso aos dados em ordem de magnitude. O algoritmo LFU pode ser essencialmente considerado como um problema K top (K = 1), ou seja, o elemento com a menor frequência é selecionado. Portanto, podemos facilmente pensar em usar um heap binário para selecionar o elemento com a menor frequência. Esta implementação é mais eficiente. A estratégia de implementação é pequena pilha superior + tabela de hash.
Usando um heap binário para encontrar a menor de todas as chaves, a complexidade de tempo é 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();
}
}
}