기술나눔

Java 로컬 캐시(고성능 설정)

2024-07-12

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

Java 세계에서 캐싱 기술은 애플리케이션 성능을 향상시키기 위해 널리 사용되며 주로 원격 캐싱과 로컬 캐싱의 두 가지 범주로 나뉩니다. 뛰어난 성능과 유연성을 갖춘 원격 캐싱은 Redis 및 Memcached와 같은 널리 사용되는 솔루션을 통해 구현되는 경우가 많습니다. 가볍고 빠른 액세스 특성을 지닌 로컬 캐시는 HashMap, Guava Cache, Caffeine 및 Ehcache와 같은 기술로 대표됩니다.

향후 블로그 게시물에서 원격 캐싱의 미스터리에 대해 자세히 알아보겠습니다. 하지만 오늘은 로컬 캐싱에 중점을 두도록 하겠습니다. 이 문서에서는 먼저 로컬 캐싱 기술을 안내하여 포괄적인 개요를 제공합니다. 다음으로 성능의 왕이라 불리는 캐싱 기술에 대해 알아보고, 그 원리와 구현 방식을 살펴보겠습니다. 마지막으로 일련의 실제 사례를 통해 일상 업무에서 이러한 고성능 로컬 캐싱 기술을 효과적으로 활용하여 개발 효율성과 애플리케이션 성능을 향상시키는 방법을 보여줍니다.

1. 자바 로컬 캐시

1.1 해시맵

Map의 기본 구현을 사용하면 직접적이고 효율적인 방법인 메모리에 직접 캐시할 객체를 저장할 수 있습니다.

이점: 이 방법은 외부 라이브러리에 의존하지 않고 간단하고 직접적이며, 캐싱 요구 사항이 복잡하지 않고 시나리오가 간단한 애플리케이션에 매우 적합합니다.

단점: 그러나 이 방법에는 자동 캐시 제거 메커니즘이 부족합니다. 고급 캐싱 전략을 구현해야 하는 경우 더 높은 맞춤형 개발 비용이 필요할 수 있습니다.

  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 구아바 캐시

Guava Cache는 LRU(Least Recent Used) 대체 알고리즘을 기반으로 Google에서 개발한 캐싱 기술입니다. 그러나 카페인의 등장으로 구아바 캐시는 점차 사람들의 시야에서 사라졌습니다. 카페인은 구아바 캐시의 장점을 계승할 뿐만 아니라 여러 측면에서 이를 능가합니다. 여기에는 구체적인 샘플 코드가 제공되지 않지만 Guava Cache에 관심이 있는 독자는 공식 웹사이트를 방문하여 자세한 내용을 확인할 수 있습니다.

이점: Guava Cache는 최대 용량 제한 설정을 지원하고 삽입 시간과 액세스 시간 기반의 두 가지 만료 전략을 제공하며 일부 기본 통계 기능도 지원합니다.

단점: Spring Boot 2와 Spring 5가 출시되면서 Guava Cache는 더 이상 둘 다에 권장되지 않습니다.

1.3 카페인

Caffeine은 LRU와 LFU(최소 주파수 사용)의 장점을 결합한 캐시 제거 전략인 W-TinyLFU 알고리즘을 사용하는 오픈 소스 캐싱 기술입니다. Caffeine의 캐시 성능은 이론적 최적 솔루션에 가깝고 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는 빠르고 효율적인 성능으로 알려진 순수 Java 프로세스 내 캐싱 프레임워크입니다. 이는 널리 알려져 있으며 Hibernate의 기본 캐시 공급자 역할을 합니다.

이점 : Ehcache는 LFU(가장 덜 자주 사용됨), LRU(가장 최근에 사용되지 않음) 및 FIFO(선입선출)를 포함한 광범위한 캐시 제거 알고리즘을 제공합니다. 다양한 스토리지 요구 사항에 맞게 힙 내 캐시, 오프 힙 캐시, 디스크 캐시 등 다양한 유형의 캐시 스토리지를 지원합니다. 또한 Ehcache는 다양한 클러스터 솔루션을 지원하여 데이터 공유 문제를 효과적으로 해결합니다.

단점: Ehcache는 여러 면에서 뛰어나지만 성능 면에서는 Caffeine에 비해 약간 부족합니다.

  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. 고성능 캐시 카페인

2.1 캐시 유형

2.1.1 캐시

 

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);

캐시 인터페이스는 명시적 검색을 사용하여 캐시된 요소를 찾고, 업데이트하고, 제거하는 기능을 제공합니다. 캐시된 요소를 생성할 수 없거나 생성 프로세스 중에 예외가 발생하여 요소 생성에 실패한 경우, 캐시.get은 null 을 반환할 수 있습니다.

2.1.2 캐시 로딩

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);

 

LoadingCache는 CacheLoader 기능이 추가된 캐시의 캐시 구현입니다.
캐시가 없으면 해당 캐시 요소가 CacheLoader.load를 통해 생성됩니다.

2.1.3 비동기 캐시

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는 비동기 형식의 캐시로, Executor가 캐시 요소를 생성하고 CompletableFuture를 반환하는 기능을 제공합니다. 기본 스레드 풀 구현은 ForkJoinPool.commonPool()이지만 Caffeine.executor(Executor) 메서드를 재정의하고 구현하여 스레드 풀 선택을 사용자 정의할 수도 있습니다.

2.1.4 비동기 로딩 캐시

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는 캐시 요소를 생성하기 위한 비동기 로드 기능을 제공하는 LoadingCache의 비동기 형식입니다.

2.2 퇴거 전략

  • 용량 기반

// 基于缓存内的元素个数进行驱逐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));
  • 시간 기반

// 基于固定的过期时间驱逐策略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));
  • 인용을 기반으로

//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 새로 고침 메커니즘

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

새로 고침 전략은 LoadingCache에서만 사용할 수 있습니다. 제거와 달리 캐시 요소를 새로 고치는 동안 쿼리하면 이전 값이 계속 반환되고 요소 새로 고침이 완료될 때까지 새로 고친 새 값이 반환되지 않습니다.

2.4 통계

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

Caffeine.recordStats() 메서드를 사용하여 데이터 수집을 켤 수 있습니다. Cache.stats() 메서드는 다음과 같은 일부 통계 표시기를 포함하는 CacheStats 개체를 반환합니다.

  • hitRate(): 쿼리 캐시 적중률

  • evictionCount(): 제거되는 캐시 수

  • averageLoadPenalty(): 새 값을 로드하는 데 걸리는 평균 시간

SpringBoot에서 제공하는 RESTful Controller를 사용하면 캐시 사용량을 쉽게 쿼리할 수 있습니다.