Compartilhamento de tecnologia

Cache local Java (configuração de alto desempenho)

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

No mundo Java, a tecnologia de cache é amplamente utilizada para melhorar o desempenho do aplicativo e é dividida principalmente em duas categorias: cache remoto e cache local. O cache remoto, com excelente desempenho e flexibilidade, é frequentemente implementado por meio de soluções populares como Redis e Memcached. O cache local, com suas características leves e de acesso rápido, é representado por tecnologias como HashMap, Guava Cache, Caffeine e Ehcache.

Iremos nos aprofundar nos mistérios do cache remoto em postagens futuras do blog, mas hoje vamos nos concentrar no cache local. Este artigo primeiro apresentará a tecnologia de cache local para fornecer uma visão geral abrangente. A seguir, nos aprofundaremos na tecnologia de cache, conhecida como a rainha do desempenho, e exploraremos os princípios e métodos de implementação por trás dela. Finalmente, através de uma série de casos práticos, demonstraremos como utilizar efetivamente essas tecnologias de cache local de alto desempenho no trabalho diário para melhorar a eficiência do desenvolvimento e o desempenho do aplicativo.

1. Cache local Java

1.1 Mapa de Hash

Usando a implementação subjacente do Map, podemos armazenar os objetos a serem armazenados em cache diretamente na memória, que é um método direto e eficiente.

Vantagem: Este método é simples e direto, sem depender de bibliotecas externas, e é muito adequado para aplicações com requisitos descomplicados de cache e cenários simples.

Desvantagens: No entanto, este método carece de um mecanismo automático de eliminação de cache. Se estratégias de cache mais avançadas precisarem ser implementadas, poderão ser necessários custos de desenvolvimento personalizados mais elevados.

  1. public class LRUCache extends LinkedHashMap {
  2. /**
  3. * 可重入读写锁,保证并发读写安全性
  4. */
  5. private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  6. private Lock readLock = readWriteLock.readLock();
  7. private Lock writeLock = readWriteLock.writeLock();
  8. /**
  9. * 缓存大小限制
  10. */
  11. private int maxSize;
  12. public LRUCache(int maxSize) {
  13. super(maxSize + 1, 1.0f, true);
  14. this.maxSize = maxSize;
  15. }
  16. @Override
  17. public Object get(Object key) {
  18. readLock.lock();
  19. try {
  20. return super.get(key);
  21. } finally {
  22. readLock.unlock();
  23. }
  24. }
  25. @Override
  26. public Object put(Object key, Object value) {
  27. writeLock.lock();
  28. try {
  29. return super.put(key, value);
  30. } finally {
  31. writeLock.unlock();
  32. }
  33. }
  34. @Override
  35. protected boolean removeEldestEntry(Map.Entry eldest) {
  36. return this.size() > maxSize;
  37. }
  38. }

1.2 Cache de Goiaba

Guava Cache é uma tecnologia de cache desenvolvida pelo Google que se baseia no algoritmo de substituição LRU (menos usado recentemente). No entanto, com o aumento da cafeína, o Goiaba Cache desapareceu gradualmente da vista das pessoas. A cafeína não só herda as vantagens do Goiaba Cache, mas também o supera em muitos aspectos. Embora o código de amostra específico não seja fornecido aqui, os leitores interessados ​​no Guava Cache podem visitar seu site oficial para obter mais informações.

Vantagem: O Guava Cache suporta a definição de limites máximos de capacidade e fornece duas estratégias de expiração: com base no tempo de inserção e no tempo de acesso, e também suporta algumas funções estatísticas básicas.

Desvantagens: Com o lançamento do Spring Boot 2 e Spring 5, o Guava Cache não é mais recomendado para ambos.

1.3 Cafeína

A cafeína é uma tecnologia de cache de código aberto que usa o algoritmo W-TinyLFU, que é uma estratégia de eliminação de cache que combina as vantagens de LRU e LFU (menor frequência usada). O desempenho do cache do Caffeine está próximo da solução teórica ideal e pode ser considerado como uma versão atualizada do Guava Cache.

  1. public class CaffeineCacheTest {
  2. public static void main(String[] args) throws Exception {
  3. //创建guava cache
  4. Cache<String, String> loadingCache = Caffeine.newBuilder()
  5. //cache的初始容量
  6. .initialCapacity(5)
  7. //cache最大缓存数
  8. .maximumSize(10)
  9. //设置写缓存后n秒钟过期
  10. .expireAfterWrite(17, TimeUnit.SECONDS)
  11. //设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite
  12. //.expireAfterAccess(17, TimeUnit.SECONDS)
  13. .build();
  14. String key = "key";
  15. // 往缓存写数据
  16. loadingCache.put(key, "v");
  17. // 获取value的值,如果key不存在,获取value后再返回
  18. String value = loadingCache.get(key, CaffeineCacheTest::getValueFromDB);
  19. // 删除key
  20. loadingCache.invalidate(key);
  21. }
  22. private static String getValueFromDB(String key) {
  23. return "v";
  24. }
  25. }

1.4 Ehcache

Ehcache é uma estrutura de cache em processo Java pura, conhecida por seu desempenho rápido e eficiente. É amplamente reconhecido e serve como provedor de cache padrão para o Hibernate.

Vantagem : Ehcache fornece uma ampla gama de algoritmos de remoção de cache, incluindo LFU (menos usado), LRU (menos usado recentemente) e FIFO (primeiro a entrar, primeiro a sair). Ele suporta diferentes tipos de armazenamento de cache, como cache in-heap, cache off-heap e cache de disco, para se adaptar a diferentes necessidades de armazenamento. Além disso, o Ehcache também oferece suporte a uma variedade de soluções de cluster, resolvendo efetivamente o problema de compartilhamento de dados.

Desvantagens: Embora o Ehcache seja excelente em muitos aspectos, ele fica um pouco aquém do Caffeine em termos de desempenho.

  1. public class EncacheTest {
  2. public static void main(String[] args) throws Exception {
  3. // 声明一个cacheBuilder
  4. CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
  5. .withCache("encacheInstance", CacheConfigurationBuilder
  6. //声明一个容量为20的堆内缓存
  7. .newCacheConfigurationBuilder(String.class,String.class, ResourcePoolsBuilder.heap(20)))
  8. .build(true);
  9. // 获取Cache实例
  10. Cache<String,String> myCache = cacheManager.getCache("encacheInstance", String.class, String.class);
  11. // 写缓存
  12. myCache.put("key","v");
  13. // 读缓存
  14. String value = myCache.get("key");
  15. // 移除换粗
  16. cacheManager.removeCache("myCache");
  17. cacheManager.close();
  18. }
  19. }

 

2. Cache de alto desempenho Cafeína

2.1 Tipo de cache

2.1.1 Cache

 

Cache<Key, Graph> cache = Caffeine.newBuilder()    .expireAfterWrite(10, TimeUnit.MINUTES)    .maximumSize(10_000)    .build();
// 查找一个缓存元素, 没有查找到的时候返回nullGraph graph = cache.getIfPresent(key);// 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回nullgraph = cache.get(key, k -> createExpensiveGraph(key));// 添加或者更新一个缓存元素cache.put(key, graph);// 移除一个缓存元素cache.invalidate(key);

A interface Cache fornece a capacidade de localizar, atualizar e remover elementos armazenados em cache usando pesquisas explícitas. Quando o elemento armazenado em cache não pode ser gerado ou uma exceção é lançada durante o processo de geração e a geração do elemento falha, cache.get pode retornar null .

2.1.2 Carregando Cache

LoadingCache<Key, Graph> cache = Caffeine.newBuilder()    .maximumSize(10_000)    .expireAfterWrite(10, TimeUnit.MINUTES)    .build(key -> createExpensiveGraph(key));
// 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回nullGraph graph = cache.get(key);// 批量查找缓存,如果缓存不存在则生成缓存元素Map<Key, Graph> graphs = cache.getAll(keys);

 

Um LoadCache é uma implementação de cache de um Cache com o recurso CacheLoader adicionado.
Se o cache não estiver presente, o elemento de cache correspondente será gerado por meio de CacheLoader.load.

2.1.3 Cache assíncrono

AsyncCache<Key, Graph> cache = Caffeine.newBuilder()    .expireAfterWrite(10, TimeUnit.MINUTES)    .maximumSize(10_000)    .buildAsync();
// 查找一个缓存元素, 没有查找到的时候返回nullCompletableFuture<Graph> graph = cache.getIfPresent(key);// 查找缓存元素,如果不存在,则异步生成graph = cache.get(key, k -> createExpensiveGraph(key));// 添加或者更新一个缓存元素cache.put(key, graph);// 移除一个缓存元素cache.synchronous().invalidate(key);

AsyncCache é a forma assíncrona de Cache, fornecendo ao Executor a capacidade de gerar elementos de cache e retornar CompletableFuture. A implementação padrão do pool de threads é ForkJoinPool.commonPool(), mas você também pode personalizar sua seleção do pool de threads substituindo e implementando o método Caffeine.executor(Executor).

2.1.4 Cache de Carregamento Assíncrono

AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()    .maximumSize(10_000)    .expireAfterWrite(10, TimeUnit.MINUTES)    // 你可以选择: 去异步的封装一段同步操作来生成缓存元素    .buildAsync(key -> createExpensiveGraph(key));    // 你也可以选择: 构建一个异步缓存元素操作并返回一个future    .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
// 查找缓存元素,如果其不存在,将会异步进行生成CompletableFuture<Graph> graph = cache.get(key);// 批量查找缓存元素,如果其不存在,将会异步进行生成CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

AsyncLoadingCache é a forma assíncrona de LoadCache, que fornece a função de carregamento assíncrono para gerar elementos de cache.

2.2 Estratégia de despejo

  • Baseado em capacidade

// 基于缓存内的元素个数进行驱逐LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .maximumSize(10_000)    .build(key -> createExpensiveGraph(key));
// 基于缓存内元素权重进行驱逐LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .maximumWeight(10_000)    .weigher((Key key, Graph graph) -> graph.vertices().size())    .build(key -> createExpensiveGraph(key));
  • baseado no tempo

// 基于固定的过期时间驱逐策略LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .expireAfterAccess(5, TimeUnit.MINUTES)    .build(key -> createExpensiveGraph(key));LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .expireAfterWrite(10, TimeUnit.MINUTES)    .build(key -> createExpensiveGraph(key));
// 基于不同的过期驱逐策略LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .expireAfter(new Expiry<Key, Graph>() {      public long expireAfterCreate(Key key, Graph graph, long currentTime) {        // Use wall clock time, rather than nanotime, if from an external resource        long seconds = graph.creationDate().plusHours(5)            .minus(System.currentTimeMillis(), MILLIS)            .toEpochSecond();        return TimeUnit.SECONDS.toNanos(seconds);      }      public long expireAfterUpdate(Key key, Graph graph,           long currentTime, long currentDuration) {        return currentDuration;      }      public long expireAfterRead(Key key, Graph graph,          long currentTime, long currentDuration) {        return currentDuration;      }    })    .build(key -> createExpensiveGraph(key));
  • baseado em citação

//key和缓存元素都不再存在其他强引用的时候驱逐LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .weakKeys()    .weakValues()    .build(key -> createExpensiveGraph(key));
// 当进行GC的时候进行驱逐LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .softValues()    .build(key -> createExpensiveGraph(key));

2.3 Mecanismo de atualização

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .maximumSize(10_000)    .refreshAfterWrite(1, TimeUnit.MINUTES)    .build(key -> createExpensiveGraph(key));

A estratégia de atualização só pode ser usada em LoadCache. Ao contrário do despejo, se o elemento de cache for consultado durante a atualização, seu valor antigo ainda será retornado e o novo valor atualizado não será retornado até que a atualização do elemento seja concluída.

2.4 Estatísticas

Cache<Key, Graph> graphs = Caffeine.newBuilder() .maximumSize(10_000) .recordStats() .build();

A coleta de dados pode ser ativada usando o método Caffeine.recordStats(). O método Cache.stats() retornará um objeto CacheStats, que conterá alguns indicadores estatísticos, como:

  • hitRate(): taxa de acerto do cache de consulta

  • evictionCount(): Número de caches sendo despejados

  • AverageLoadPenalty(): O tempo médio que leva para um novo valor ser carregado

Com o controlador RESTful fornecido pelo SpringBoot, você pode consultar facilmente o uso do cache.