Cache local de cafeína

1. Introdução ao Caffine

Simplificando, o Caffeine é um componente de cache local de alto desempenho.
Pode ser visto nas três figuras a seguir: Não importa no cenário de leitura simultânea, gravação simultânea ou leitura e gravação simultâneas, o desempenho do Caffeine está significativamente à frente de outros locais componentes de cache de código aberto.

insira a descrição da imagem aqui

insira a descrição da imagem aqui
insira a descrição da imagem aqui

2. Algoritmos comuns de eliminação de cache

2.1、FIFO

É o primeiro a eliminar os primeiros dados armazenados em cache e é o algoritmo de eliminação mais simples. A desvantagem é que, se os primeiros dados armazenados em cache forem usados ​​com mais frequência, os dados continuarão entrando e saindo, portanto, a taxa de acertos do cache é relativamente baixa

2.2、LRU

É eliminar primeiro os dados que não são acessados ​​há mais tempo. A desvantagem é que ele não pode lidar bem com o tráfego intermitente ocasional. Por exemplo, se um dado for acessado várias vezes nos primeiros 59 segundos de um minuto, mas não for acessado no último segundo, mas um lote de dados impopulares entrar no cache no último segundo, os dados quentes serão lavado

2.3、LFU

Usado menos recentemente. Dá prioridade à eliminação dos dados menos utilizados e necessita de manter um campo que indique a frequência de utilização, apresentando duas desvantagens principais:

É necessário manter informações de frequência para cada item de registro, que precisa ser atualizado toda vez que for acessado, o que é uma sobrecarga enorme. A
resposta ao tráfego esparso repentino é lenta, porque os dados históricos acumularam muitas contagens e novos dados devem ser classificado. No acompanhamento,
por exemplo, as músicas antigas de um determinado cantor têm muita história tocando. Se as novas músicas forem classificadas junto com as músicas antigas, elas nunca terão um futuro brilhante.

2.4、W-TinyLFU

insira a descrição da imagem aqui

Use o algoritmo Count-Min Sketch para reduzir o consumo de memória causado por informações de frequência Problema
do algoritmo de estatística de frequência tradicional
: Se o chefe solicitar que você conte a frequência de elementos em um fluxo de dados em tempo real e você estiver pronto para responder à frequência de um elemento a qualquer momento, sem necessidade E quanto à contagem exata?

A intuição nos diz que podemos precisar de um HashMap enorme para contar a frequência de ocorrência de cada elemento, mas como o número de elementos diferentes pode ser muito grande, de modo que seja uma figura astronômica, a memória necessária pode ser muito grande, o que é impraticável. Ao mesmo tempo, somos obrigados a calcular e responder em tempo real. Quando o conflito do HashMap é alto, a complexidade de tempo do pior caso pode não atender aos requisitos de tempo real

Algoritmo Count-Min Sketch
O algoritmo Count-min Sketch é um algoritmo que pode ser usado para contagem. Quando o tamanho dos dados é muito grande, é um algoritmo de contagem eficiente que melhora a eficiência sacrificando a precisão. A ideia básica: criar um
comprimento
de x Array, usado para contagem, inicializa o valor de contagem de cada elemento em 0; para um novo elemento, hashes para um número entre 0 e x, como o valor de hash i, como o índice de posição do array; neste momento, o valor de contagem do índice de posição i correspondente ao array é aumentado em 1; então, para consultar a frequência de ocorrência de um elemento neste momento, basta retornar o valor de contagem do índice de posição do array correspondente ao elemento após esperança ; considerando o uso de Hash, haverá conflitos, ou seja, elementos diferentes são hash para o índice de posição do mesmo array, de modo que as estatísticas de frequência serão muito grandes

Como otimizar
o uso de múltiplos arrays e múltiplas funções hash para calcular o índice de posição de um array correspondente a um elemento; então, ao consultar a frequência de um elemento, retornar o valor mínimo dos valores de contagem deste elemento em diferentes arrays É isso;
————————————————
O algoritmo Count–Min Sketch é semelhante à ideia do Bloom Filter (Filtro Bloom), na verdade não precisamos de um valor preciso para estatísticas de frequência. Ao armazenar dados, após várias operações de função hash serem executadas na chave, a frequência de armazenamento em diferentes posições da matriz bidimensional (a Cafeína realmente usa uma matriz longa unidimensional e cada número longo é dividido em 16 partes, cada uma com 4 bits, por padrão, 15 vezes é a frequência de acesso mais alta e cada chave é, na verdade, hash quatro vezes, o que cai em algum lugar entre as 16 cópias de diferentes números longos). Ao ler os tempos de acesso de uma determinada tecla, os valores de frequência em todas as posições serão comparados, e o valor mínimo será retornado. Para resolver o problema de que o modo de acesso aos dados muda com o tempo e evitar o crescimento infinito da contagem, existe um valor máximo para a soma das frequências de acesso de todas as chaves. Quando o valor máximo é atingido, um reset será realizada, ou seja, a frequência de cada chave de cache é dividida por 2

insira a descrição da imagem aqui

2.5. Projeto de janela

O TinyLFU tem problemas com "explosões esparsas" do mesmo objeto. Nesse caso, novas rajadas de chaves não podem ser criadas com frequência suficiente para permanecer no cache, resultando em perdas de cache constantes. A cafeína resolve esse problema projetando uma estratégia chamada Window Tiny LFU (W-TinyLFU) que consiste em duas áreas de cache

O armazenamento da frequência de acesso ao cache é dividido principalmente em duas partes, LRU e LRU segmentado. Os dados recém-acessados ​​entrarão na primeira LRU, que é chamada de WindowDeque no Caffeine. Quando o WindowDeque estiver cheio, ele entrará no ProbationDeque no LRU Segmentado, e quando for acessado posteriormente, será promovido ao ProtectedDeque. Quando ProtectedDeque estiver cheio, haverá dados rebaixados para ProbationDeque. Quando os dados precisam ser eliminados, os dados em ProbationDeque devem ser eliminados. Mecanismo específico de eliminação: Pegar cabeça e rabinho do time no ProbationDeque para PK. O dado do chefe do time é o primeiro a entrar na fila, chamado de vítima, e o dado do final do time é chamado de atacante. Compare a frequência dos dois, e o grande vence e o pequeno é eliminado;
———————————————

3. Use

3.1, carregando

Deixe-me primeiro falar sobre o que é "carregamento". Ao consultar o cache, se o cache falhar, você precisa consultar em um banco de dados de terceiros e, em seguida, armazenar os dados consultados no cache primeiro e, em seguida, retorná-los ao consultador . Este processo está carregando

Caffeine fornece quatro estratégias de adição de cache: carregamento manual, carregamento automático, carregamento assíncrono manual e carregamento assíncrono automático
Adicionar dependências do Maven

 <!-- caffeine缓存框架 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.8.8</version>
</dependency>

3.1.1. Carregamento manual

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;

public class TestCache {
    
    
    public static void main(String[] args) {
    
    
        // 初始化缓存,设置了 1 分钟的写过期,100 的缓存最大个数
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .maximumSize(100)
                .build();

        int key = 1;
        // 使用 getIfPresent 方法从缓存中获取值。如果缓存中不存指定的值,则方法将返回 null
        System.out.println("不存在值,返回null:" + cache.getIfPresent(key));

        // 也可以使用 get 方法获取值,该方法将一个参数为 key 的 Function 作为参数传入。
        // 如果缓存中不存在该 key 则该函数将用于提供默认值,该值在计算后插入缓存中:
        System.out.println("返回默认值:" + cache.get(key, a -> 2));

        // 校验 key 对应的 value 是否插入缓存中
        System.out.println("返回key对应的value:" + cache.getIfPresent(key));

        // 手动 put 数据填充缓存中
        int value = 2;
        cache.put(key, value);

        // 使用 getIfPresent 方法从缓存中获取值。如果缓存中不存指定的值,则方法将返回 null
        System.out.println("返回key对应的value:" + cache.getIfPresent(key));

        // 移除数据,让数据失效
        cache.invalidate(key);
        System.out.println("返回key对应的value:" + cache.getIfPresent(key));
    }
}

insira a descrição da imagem aqui

A interface Cache fornece a capacidade de pesquisar explicitamente para localizar, atualizar e remover elementos de cache.Elementos de cache
podem ser adicionados ao cache chamando o método cache.put(key, value). Se a chave especificada no cache já tiver um elemento de cache correspondente, o elemento de cache anterior será substituído diretamente. Portanto, o elemento a ser armazenado em cache é inserido no cache por meio de cálculo atômico através de cache.get(key, k -> value) para evitar competição com outras gravações

3.1.2. Carregamento automático

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class TestCache {
    
    
    public static void main(String[] args) {
    
    
        // 自动加载
        LoadingCache<String, Object> cache2 = Caffeine
                .newBuilder()
                .maximumSize(10_000)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .build(TestCache::createObject);

        String key2 = "dragon";
        // 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回 null
        Object value = cache2.get(key2);
        System.out.println(value);

        List<String> keys = new ArrayList<>();
        keys.add("dragon1");
        keys.add("dragon2");
        // 批量查找缓存,如果缓存不存在则生成缓存元素
        Map<String, Object> objectMap = cache2.getAll(keys);
        System.out.println(objectMap);
    }

    private static Object createObject(String key) {
    
    
        return "hello caffeine 2022";
    }
}

3.1.3, manual assíncrono

@Test
public void test() throws ExecutionException, InterruptedException {
    
    
    AsyncCache<String, Integer> cache = Caffeine.newBuilder().buildAsync();

    // 会返回一个 future对象, 调用 future 对象的 get 方法会一直卡住直到得到返回,和多线程的 submit 一样
    CompletableFuture<Integer> ageFuture = cache.get("张三", name -> {
    
    
        System.out.println("name:" + name);
        return 18;
    });

    Integer age = ageFuture.get();
    System.out.println("age:" + age);
}

insira a descrição da imagem aqui

3.1.4, automático assíncrono

import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class TestCache {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
                .expireAfterWrite(2, TimeUnit.SECONDS)
                .buildAsync(key -> key + "-" + System.currentTimeMillis());

        // 获取不存在的 key 时,会使用 buildAsync() 方法中的算法计算出 value,存入缓存
        // 异步获取的结果存放在 CompletableFuture 对象中,可以使用 thenAccept() 获取结果
        CompletableFuture<Object> future = cache.get("key1");
        future.thenAccept(o -> System.out.println(System.currentTimeMillis() + "-" + o));

        TimeUnit.SECONDS.sleep(3);

        // 不存在则返回 null
        CompletableFuture<Object> key2 = cache.getIfPresent("key2");
        System.out.println(key2);
    }
}

insira a descrição da imagem aqui

3.2. Propriedades

3.2.1. Capacidade de cache inicial

initialCapacity : um inteiro, indicando quantos objetos de cache podem ser armazenados

3.2.2. Capacidade máxima

maximumSize : A capacidade máxima. Se a quantidade de dados no cache exceder esse valor, o Caffeine terá um thread assíncrono dedicado a limpar o cache e limpar o cache em excesso de acordo com a estratégia de limpeza especificada. Nota: Por exemplo, se a capacidade máxima for 2, 2 dados foram armazenados neste momento e o terceiro dado é armazenado neste momento, acionando o encadeamento assíncrono para limpar o cache. Antes que a operação de limpeza seja concluída, ainda há 3 dados no cache e todos os 3 dados podem ser lidos, e o tamanho do cache também é 3. Somente quando a operação do cache é concluída, restam apenas 2 dados no cache. Quanto a quais dados é compensado, depende da estratégia de compensação

3.2.3. Peso máximo

maximumWeight: peso máximo, cada elemento armazenado no cache deve ter um valor de peso, quando o valor do peso de todos os elementos no cache exceder o peso máximo, a limpeza assíncrona será acionada

package caffeineTest;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Person {
    
    
    Integer age;
    String name;
}

aula de teste

package caffeineTest;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class Test {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Caffeine<String, Person> caffeine = Caffeine.newBuilder()
                .maximumWeight(30)
                .weigher((String key, Person value)-> value.getAge());
        Cache<String, Person> cache = caffeine.build();

        cache.put("one", new Person(12, "one"));
        cache.put("two", new Person(18, "two"));
        cache.put("three", new Person(1, "three"));

        Thread.sleep(10);
        System.out.println(cache.estimatedSize());
        System.out.println(cache.getIfPresent("one"));
        System.out.println(cache.getIfPresent("two"));
        System.out.println(cache.getIfPresent("three"));
    }
}

insira a descrição da imagem aqui

Se você deseja usar o peso para medir, deve especificar qual é o peso e como calcular o peso de cada elemento. O método do pesador é definir as regras de peso. Seu parâmetro é uma função. Os parâmetros da função são chave e valor. O valor de retorno da função é o peso do elemento

Por exemplo, no código acima, a cafeína define o valor de peso máximo como 30 e, em seguida, usa a idade de cada objeto Pessoa como o valor de peso, portanto, todo o significado é: Os objetos Pessoa são armazenados no cache, mas a soma da idade de todos os objetos não pode exceder 30 , caso contrário, aciona uma limpeza de cache assíncrona.
Preste atenção especial a um ponto: a capacidade máxima e o peso máximo só podem ser selecionados como limite do espaço do cache

4. Status do cache

Por padrão, o status do cache será registrado com um objeto CacheStats. Ao acessar o objeto CacheStats, você pode conhecer vários indicadores de status do cache atual. Então, quais indicadores existem?
totalLoadTime: tempo total de carregamento
loadFailureRate: taxa de falha de carregamento = tempos totais de falha de carregamento / tempos totais de carregamento
averageLoadPenalty: tempo médio de carregamento, unidade, nanossegundos
evictionCount: número total de dados eliminados do cache
evictionWeight: dados eliminados do cache Peso total
hitCount: Número de acertos do cache
hitRate: Taxa de cache de ocorrências
loadCount: Tempos de carregamento
loadFailureCount: Tempos de falha no carregamento loadSuccessCount: Tempos
de sucesso no carregamento
missCount: Tempos ausentes
missRate: Taxa ausente
requestCount: Número total de consultas de solicitação do usuário

4.1, o coletor de status de cache padrão CacheStats

A classe CacheStats contém 2 métodos, vamos dar uma olhada:
CacheStats minus(@Nonnull CacheStats other): Os indicadores do objeto CacheStats atual menos os indicadores do parâmetro other, e a diferença forma um novo objeto CacheStats CacheStats
plus(@Nonnull CacheStats other): Os indicadores do objeto CacheStats atual mais os indicadores do parâmetro other e o valor formam um novo objeto CacheStats

O verdadeiro conhecimento vem da prática

package caffeineTest;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.stats.CacheStats;

public class Test {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Caffeine<String, Person> caffeine = Caffeine.newBuilder()
                .maximumWeight(30)
                .recordStats()
                .weigher((String key, Person value)-> value.getAge());
        Cache<String, Person> cache = caffeine.build();
        cache.put("one", new Person(12, "one"));
        cache.put("two", new Person(18, "two"));
        cache.put("three", new Person(1, "three"));
        Person one = cache.getIfPresent("one");
        System.out.println(one);
        CacheStats stats = cache.stats();
        System.out.println(stats.hitCount());
    }
}

insira a descrição da imagem aqui

4.2. Coletor de status de cache personalizado

A função do coletor de status do cache personalizado: Sempre que houver uma operação no cache, seja consultando, carregando ou armazenando, alguns indicadores de status do cache serão alterados e quais indicadores de status foram alterados acionarão automaticamente a coleta O método correspondente no servidor é executado. Se o código que personalizamos no método é o código de coleção, como enviar o valor do indicador para o Kafka, outros programas podem ler o valor do Kafka e, em seguida, analisá-lo e visualizá-lo para realizar o cache O real -tempo de monitoramento da
interface do coletor é StatsCounter, só precisamos implementar todos os métodos abstratos desta interface

package caffeineTest;

import com.github.benmanes.caffeine.cache.stats.CacheStats;
import com.github.benmanes.caffeine.cache.stats.StatsCounter;

public class MyStatsCounter implements StatsCounter {
    
    
    @Override
    public void recordHits(int i) {
    
    
        System.out.println("命中次数:" + i);
    }

    @Override
    public void recordMisses(int i) {
    
    
        System.out.println("未命中次数:" + i);
    }

    @Override
    public void recordLoadSuccess(long l) {
    
    
        System.out.println("加载成功次数:" + l);
    }

    @Override
    public void recordLoadFailure(long l) {
    
    
        System.out.println("加载失败次数:" + l);
    }

    @Override
    public void recordEviction() {
    
    
        System.out.println("因为缓存大小限制,执行了一次缓存清除工作");
    }

    @Override
    public void recordEviction(int weight) {
    
    
        System.out.println("因为缓存权重限制,执行了一次缓存清除工作,清除的数据的权重为:" + weight);
    }

    @Override
    public CacheStats snapshot() {
    
    
        return null;
    }
}

Atenção especial deve ser dada: os valores de estado obtidos por esses métodos no coletor são apenas os resultados da operação de cache atual, como o cache atual. O parâmetro i = 1, porque esta operação atinge 1 vez

package caffeineTest;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class Test {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Caffeine<String, Person> caffeine = Caffeine.newBuilder()
                .maximumWeight(30)
                .recordStats(MyStatsCounter::new)
                .weigher((String key, Person value) -> value.getAge());
        Cache<String, Person> cache = caffeine.build();
        cache.put("one", new Person(12, "one"));
        cache.put("two", new Person(18, "two"));
        cache.put("three", new Person(1, "three"));
        cache.getIfPresent("ww");
        Thread.sleep(1000);
    }
}

insira a descrição da imagem aqui

5. Conjunto de threads

O pool de buffers Caffeine sempre tem algumas tarefas assíncronas para executar, portanto inclui um pool de threads para executar essas tarefas assíncronas. O pool de threads padrão é ForkJoinPool.commonPool(); se você precisar usar outros pools de threads, poderá usar o método executor () configuração, o parâmetro do método é um objeto de pool de threads

insira a descrição da imagem aqui

6. Política de expiração de dados

6.1、expireAfterAccess

Após a última visita, se não for visitado novamente, expirará. O acesso inclui leitura e escrita. por exemplo:

package caffeineTest;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.TimeUnit;

public class Test {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Caffeine<String, Person> caffeine = Caffeine.newBuilder()
                .maximumWeight(30)
                .expireAfterAccess(2, TimeUnit.SECONDS)
                .weigher((String key, Person value)-> value.getAge());
        Cache<String, Person> cache = caffeine.build();
        cache.put("one", new Person(12, "one"));
        cache.put("two", new Person(18, "two"));
        System.out.println(cache.getIfPresent("one"));
        System.out.println(cache.getIfPresent("two"));
        Thread.sleep(3000);
        System.out.println(cache.getIfPresent("one"));
        System.out.println(cache.getIfPresent("two"));
    }
}

insira a descrição da imagem aqui

6.2, expire AfterWrite

Quanto tempo depois de um determinado dado não ser atualizado, ele expira. por exemplo

package caffeineTest;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.TimeUnit;

public class Test {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Caffeine<String, Person> caffeine = Caffeine.newBuilder()
                .maximumWeight(30)
                .expireAfterWrite(2, TimeUnit.SECONDS)
                .weigher((String key, Person value)-> value.getAge());
        Cache<String, Person> cache = caffeine.build();
        cache.put("one", new Person(12, "one"));
        cache.put("two", new Person(18, "two"));
        Thread.sleep(1000);
        System.out.println(cache.getIfPresent("one").getName());
        Thread.sleep(2000);
        System.out.println(cache.getIfPresent("one"));
    }
}

insira a descrição da imagem aqui

A vida útil dos dados só pode ser estendida com a atualização, mesmo que os dados sejam lidos, não funcionará e expirará quando chegar a hora

6.3、expiraDepois

Personalize a estratégia de cache para atender a diversos requisitos de tempo de expiração

package caffeineTest;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Expiry;

public class Test {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Caffeine<String, Person> caffeine = Caffeine.newBuilder()
            .maximumWeight(30)
            .expireAfter(new Expiry<String, Person>() {
    
    
                @Override
                public long expireAfterCreate(String s, Person person, long l) {
    
    
                    // 首次存入缓存后,年龄大于 60 的,过期时间为 4 秒
                    if(person.getAge() > 60){
    
    
                        return 4000000000L;
                    }
                    return 2000000000L; // 否则为 2 秒
                }

                @Override
                public long expireAfterUpdate(String s, Person person, long l, long l1) {
    
    
                    // 更新 one 这个人之后,过期时间为 8 秒
                    if(person.getName().equals("one")){
    
    
                        return 8000000000L;
                    }
                    return 4000000000L; // 更新其它人后,过期时间为 4 秒
                }

                @Override
                public long expireAfterRead(String s, Person person, long l, long l1) {
    
    
                    // 每次被读取后,过期时间为 3 秒
                    return 3000000000L;
                }
            })
            .weigher((String key, Person value)-> value.getAge());
        Cache<String, Person> cache = caffeine.build();
    }
}

6.4. Reciclagem baseada em referências

Adicione uma descrição da imagem

package caffeineTest;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;

public class Test {
    
    
    @org.junit.Test
    public void referenceTest() {
    
    
        LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
                .weakKeys()
                .weakValues()
                .build(this::buildLoader);
    }

    public String buildLoader(String k) {
    
    
        return k + "+default";
    }
}

Caffeine.weakKeys(): Use referências fracas para armazenar chaves. Se não houver nenhuma referência forte para a chave em outro lugar, o cache será recuperado pelo coletor de lixo. Caffeine.weakValues(): Use
referências fracas para armazenar valores. Se não houver nenhuma referência forte ao valor em outro lugar, o cache será recuperado pelo coletor de lixo
Caffeine.softValues(): Use referências suaves para armazenar valores. Quando a memória estiver cheia, o objeto da referência suave será coletado como lixo da maneira menos usada recentemente
Nota: Caffeine.weakValues() e Caffeine.softValues() não podem ser usados ​​juntos
——————————— —— ————

6.5, atualização automática

refreshAfterWrite(long duration, TimeUnit unit): Quanto tempo leva para atualizar os dados no cache após a conclusão da operação de gravação. Os dois parâmetros são usados ​​apenas para definir o período de tempo. Aplicável apenas a LoadingCache e AsyncLoadingCache, se a operação de atualização não for concluída, os dados lidos serão apenas dados antigos

package caffeineTest;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.util.concurrent.TimeUnit;

public class Test {
    
    
    @org.junit.Test
    public void referenceTest() {
    
    
        LoadingCache<String, String> graphs = Caffeine.newBuilder()
                .maximumSize(10000)
                // 指定在创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
                .refreshAfterWrite(1, TimeUnit.MINUTES)
                .build(this::buildLoader);
    }

    public String buildLoader(String k) {
    
    
        return k + "+default";
    }
}

6.6, remova o ouvinte

Quando os dados no cache forem atualizados ou limpos, o listener será acionado.No listener, alguns métodos de processamento podem ser personalizados, como imprimir quais dados são limpos e por quê. O processo de disparo e monitoramento é assíncrono, ou seja, pode demorar um pouco para que os dados sejam apagados antes que o ouvinte detecte. O verdadeiro conhecimento vem da
prática

package caffeineTest;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;

public class Test {
    
    
    @org.junit.Test
    public void referenceTest() throws InterruptedException {
    
    
        MyStatsCounter myStatsCounter = new MyStatsCounter();
        Caffeine<String, Person> caffeine = Caffeine.newBuilder()
                .maximumWeight(30)
                .removalListener((String key, Person value, RemovalCause cause) -> {
    
    
                    System.out.println("被清除人的年龄:" + value.getAge() + ";  清除的原因是:" + cause);
                })
                .weigher((String key, Person value) -> value.getAge());
        Cache<String, Person> cache = caffeine.build();
        cache.put("one", new Person(12, "one"));
        cache.put("two", new Person(18, "two"));
        cache.put("one", new Person(14, "one"));
        cache.invalidate("one");
        cache.put("three", new Person(31, "three"));
        Thread.sleep(2000);
    }
}

O parâmetro do método removeListener é um objeto RemovalListener, mas pode ser passado de forma funcional. Como no código acima, quando os dados são atualizados ou apagados, três conteúdos serão fornecidos ao ouvinte, (chave, valor, motivo ) correspondendo respectivamente aos três no código Cada parâmetro, (chave, valor) é o valor antigo antes de atualizar e limpar, para que você possa entender os detalhes da compensação. Existem 5 motivos para remoção, que são armazenados na classe de enumeração RemovalCause:

EXPLICIT: Indica que a operação de exclusão é chamada explicitamente e certos dados são excluídos diretamente
REPLACED: Indica que certos dados são atualizados
EXPIRED: Indica que foi limpo porque o ciclo de vida acabou (o tempo de expiração acabou),
TAMANHO: Indica que o espaço do cache é limitado, O peso total é limitado e limpo
COLLECTED: usado para nosso coletor de lixo, ou seja, as referências suaves que reduzimos acima, referências fracas

insira a descrição da imagem aqui

6.7, ouvinte síncrono

O RemovaListener anterior é um monitoramento assíncrono. O método Writer aqui pode definir um ouvinte síncrono. O ouvinte síncrono é um objeto instanciado que implementa a interface CacheWriter. Precisamos personalizar a classe de implementação da interface, como:

package caffeineTest;

import com.github.benmanes.caffeine.cache.CacheWriter;
import com.github.benmanes.caffeine.cache.RemovalCause;

public class MyCacheWriter implements CacheWriter<String, Person> {
    
    
    @Override
    public void write(String s, Person person) {
    
    
        System.out.println("新增/更新了一个新数据:" + person.getName());
    }

    @Override
    public void delete(String s, Person person, RemovalCause removalCause) {
    
    
        System.out.println("删除了一个数据:" + person.getName());
    }
}

A chave é implementar os dois métodos da interface CacheWriter. Quando um novo dado é adicionado ou atualizado, a execução do método write será acionada de forma síncrona. Quando um determinado dado é excluído, a execução do método delete será acionada

package caffeineTest;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class Test {
    
    
    @org.junit.Test
    public void referenceTest() throws InterruptedException {
    
    
        Caffeine<String, Person> caffeine = Caffeine.newBuilder()
                .maximumWeight(30)
                .writer(new MyCacheWriter())
                .weigher((String key, Person value) -> value.getAge());
        Cache<String, Person> cache = caffeine.build();
        cache.put("one", new Person(12, "one"));
        cache.put("two", new Person(18, "two"));
        cache.invalidate("two");
    }
}

insira a descrição da imagem aqui

6.7、API

V getIfPresent(K key): Se a chave existir no cache, obtém o valor, caso contrário retorna null
void put(K key, V value): Armazena um par de dados <key, value>
Map<K, V> getAllPresent( Iterable< ?> var1): O parâmetro é um iterador, indicando que o cache pode ser consultado em lotes void putAll(Map<? extends K, ? extends V> var1); batch armazenado no cache void invalidate(K var1): exclui os dados correspondentes a uma determinada chave void invalidateAll(Iterable<?> var1): Exclui dados em lotes
void invalidateAll(): Limpa o cache
long estimaSize(): Retorna o número de dados no cache
CacheStats stats(): Retorna o conjunto de índices de status atual do
cache ConcurrentMap<K, V> asMap(): Constrói todos os dados no cache em um mapa
void cleanUp(): O cache será limpo como um todo. Por exemplo, alguns dados expiraram, mas não será limpo imediatamente, então executar o método cleanUp uma vez limpará o cache Faça uma verificação para limpar os dados que devem ser limpos
V get(K var1, Function<? super K, ? extends V> var2): O primeiro parâmetro é a chave que você deseja obter e o segundo parâmetro é a função

7, SpringBoot Cafeína

Introduzir dependências

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
        <version>2.6.2</version>
    </dependency>
</dependencies>

Adicione o seguinte conteúdo ao arquivo de configuração application.properties dentro do projeto de estrutura SpringBoot
spring.cache.cache-names=userCache
spring.cache.caffeine.spec=initialCapacity=50, maximumSize=500, expireAfterWrite=10s
server.port=8080

classe de inicialização

package example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class CacheApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(CacheApplication.class,args);
    }
}

classe de configuração

package example.config;

import com.github.benmanes.caffeine.cache.CacheLoader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CacheConfig {
    
    
    @Bean
    public CacheLoader<String,Object> cacheLoader(){
    
    
        return new CacheLoader<String, Object>() {
    
    
            @Override
            public Object load(String s) throws Exception {
    
    
                return null;
            }

            @Override
            public Object reload(String key, Object oldValue) throws Exception {
    
    
                return oldValue;
            }
        };
    }
}
package example;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
public class UserDao {
    
    
    private Cache<Long, User> userCache = Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(100, TimeUnit.SECONDS)
            .build();

    public User queryByUserIdV2(long userId) {
    
    
        userCache.get(userId, aLong -> {
    
    
            System.out.println("用户本地缓存不存在,重新计算");
            return new User();
        });
        return userCache.getIfPresent(userId);
    }

    public boolean insertUser(int userId) {
    
    
        User user = new User();
        user.setId(userId);
        user.setTel("11111");
        userCache.put((long) userId, user);
        return true;
    }

    @Cacheable(value = "userCache", key = "#userId", sync = true)
    public User queryByUserId(int userId) {
    
    
        System.out.println("从数据库查询userId");
        User user = new User();
        user.setId(1001);
        user.setTel("18971823123");
        user.setUsername("idea");
        return user;
    }

    /**
     * sync 用于同步的,在缓存失效(过期不存在等各种原因)的时候,如果多个线程同时访问被标注的方法
     * 则只允许一个线程通过去执行方法
     */
    @CachePut(value = "userCache", key = "#user.id")
    public void saveUser(User user) {
    
    
        System.out.println("插入数据库一条用户记录");
    }

    @CacheEvict(value = "userCache", key = "#userId")
    public void delUser(int userId) {
    
    
        System.out.println("删除用户本地缓存");
    }
}

Explique o significado real de cada anotação usada dentro do UserDao:
@Cacheable
Após cada consulta ser executada, os dados serão armazenados em uma coleção de mapas local e, quando a segunda solicitação for feita, se a chave correspondente existir no cache local, então o método de consulta real não é penalizado

Toda vez que @CachePut
acionar um método com a anotação CachePut, ele colocará os parâmetros da requisição no cache local, mas preste atenção em um atributo interno de configuração de sincronização. Quando o cache não existir localmente, a requisição irá inserir a anotação de declaração correspondente no caso de multi-threading, pode haver métodos correspondentes a solicitações simultâneas de vários threads. Neste momento, você pode limitá-lo usando o atributo de sincronização.

sync = true: Apenas um thread pode atualizar o cache local em acesso simultâneo.
sync = false: atualiza o cache local simultaneamente

@CacheEvict
é usado para excluir a configuração de cache especificada e a exclusão é baseada no atributo de chave correspondente

As propriedades básicas do objeto Usuário são as seguintes:

package example;

public class User {
    
    
    private int id;
    private String username;
    private String tel;

    public int getId() {
    
    
        return id;
    }

    public void setId(int id) {
    
    
        this.id = id;
    }

    public String getUsername() {
    
    
        return username;
    }

    public void setUsername(String username) {
    
    
        this.username = username;
    }

    public String getTel() {
    
    
        return tel;
    }

    public void setTel(String tel) {
    
    
        this.tel = tel;
    }

    @Override
    public String toString() {
    
    
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", tel='" + tel + '\'' +
                '}';
    }
}

Controlador

package example;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;

@RestController
public class UserController {
    
    
    @Resource
    private UserDao userDao;

    @GetMapping(value = "/queryUser")
    public String queryUser(int id) {
    
    
        User user = userDao.queryByUserId(id);
        return user.toString();
    }


    @GetMapping(value = "/insertUser")
    public String insertUser(int id) {
    
    
        User user = new User();
        user.setId(id);
        user.setUsername(UUID.randomUUID().toString());
        user.setTel(String.valueOf(ThreadLocalRandom.current().nextInt()));
        userDao.saveUser(user);
        return "success";
    }

    @GetMapping(value = "/delUser")
    public String delUser(int id) {
    
    
        userDao.delUser(id);
        return "delete-success";
    }

    @GetMapping(value = "/queryUser-02")
    public String queryUser_02(long userId) {
    
    
        User user = userDao.queryByUserIdV2(userId);
        return user.toString();
    }

    @GetMapping(value = "/insertUser-02")
    public String insertUser_02(int userId) {
    
    
        try {
    
    
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        userDao.insertUser(userId);
        return "success";
    }
}

Teste se o cache local é eficaz:
solicite a interface UserController#queryUser, o endereço da solicitação é o seguinte:
http://localhost:8080/queryUser?id=1001
A primeira solicitação encontrará a saída de palavra-chave relevante no console:
insira a imagem descrição aqui
insira a descrição da imagem aqui
e então A segunda solicitação descobrirá que a lógica não acionará a execução da função queryByUserId de UserDao novamente. Isso ocorre porque a anotação @Cacheable é adicionada ao topo da função queryByUserId do UserDao
insira a descrição da imagem aqui

Acho que você gosta

Origin blog.csdn.net/chuige2013/article/details/130940330
Recomendado
Clasificación