Guava: 강력한 로컬 캐싱 프레임워크인 Cache

Guava Cache는 매우 뛰어난 로컬 캐싱 프레임워크입니다.

1. 클래식 구성

Guava Cache의 데이터 구조는 JDK1.7의 ConcurrentHashMap과 유사하며, 시간, 용량, 참조에 따른 3가지 재활용 전략과 자동 로딩, 접속 통계 등의 기능을 제공합니다.

기본 구성

    @Test
    public void testLoadingCache() throws ExecutionException {
        CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println("加载 key:" + key);
                return "value";
            }
        };

        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                //最大容量为100(基于容量进行回收)
                .maximumSize(100)
                //配置写入后多久使缓存过期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //配置写入后多久刷新缓存
                .refreshAfterWrite(1, TimeUnit.SECONDS)
                .build(cacheLoader);

        cache.put("Lasse", "穗爷");
        System.out.println(cache.size());
        System.out.println(cache.get("Lasse"));
        System.out.println(cache.getUnchecked("hello"));
        System.out.println(cache.size());

    }

예시에서는 최대 캐시 용량을 100( 용량 기준 재활용 )으로 설정하고 무효화 정책새로 고침 정책을 구성했습니다 .

1. 실패 전략

구성된  경우 expireAfterWrite 캐시 항목은 생성되거나 마지막으로 업데이트된 후 지정된 시간 내에 만료됩니다.

2. 새로 고침 전략

refreshAfterWrite 캐시된 항목이 만료되면 새 값을 다시 로드할 수 있도록 새로 고침 시간을 구성합니다  .

이 예에서 일부 학생은 새로 고침 전략을 구성해야 하는 이유는 무엇입니까?무효화 전략만 구성하는 것만으로는 충분하지 않습니까 ? 라는 질문을 가질 수 있습니다.

물론 가능하지만 동시성이 높은 시나리오에서는 새로 고침 전략을 구성하는 것이 기적적일 것입니다.다음으로 Gauva Cache의 스레드 모델에 대한 모든 사람의 이해를 돕기 위해 테스트 사례를 작성하겠습니다.

2. 스레드 모델 이해

멀티 스레드 시나리오에서 "캐시 만료 및 로드 메서드 실행"과 "새로 고침 및 다시 로드 메서드 실행"의 작업을 시뮬레이션합니다.

@Test
    public void testLoadingCache2() throws InterruptedException, ExecutionException {
        CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                return "value_" + key.toLowerCase();
            }

            @Override
            public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                Thread.sleep(500);
                return super.reload(key, oldValue);
            }
        };
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                //最大容量为20(基于容量进行回收)
                .maximumSize(20)
                //配置写入后多久使缓存过期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //配置写入后多久刷新缓存
                .refreshAfterWrite(1, TimeUnit.SECONDS)
                .build(cacheLoader);

        System.out.println("测试过期加载 load------------------");

        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        long start = System.currentTimeMillis();
                        System.out.println(Thread.currentThread().getName() + "开始查询");
                        String hello = cache.get("hello");
                        long end = System.currentTimeMillis() - start;
                        System.out.println(Thread.currentThread().getName() + "结束查询 耗时" + end);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }

        cache.put("hello2", "旧值");
        Thread.sleep(2000);
        System.out.println("测试重新加载 reload");
        //等待刷新,开始重新加载
        Thread.sleep(1500);
        ExecutorService executorService2 = Executors.newFixedThreadPool(5);
//        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        for (int i = 0; i < 5; i++) {
            executorService2.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        long start = System.currentTimeMillis();
                        System.out.println(Thread.currentThread().getName() + "开始查询");
                        //cyclicBarrier.await();
                        String hello = cache.get("hello2");
                        System.out.println(Thread.currentThread().getName() + ":" + hello);
                        long end = System.currentTimeMillis() - start;
                        System.out.println(Thread.currentThread().getName() + "结束查询 耗时" + end);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
        Thread.sleep(9000);
    }

 실행 결과는 아래 그림과 같습니다.

실행 결과는 다음과 같습니다. Guava Cache에는 로드 또는 다시 로드 메서드를 비동기적으로 실행하는 백그라운드 작업 스레드가 없습니다.

  1. 무효화 전략 : expireAfterWrite 하나의 스레드가 로드 메서드를 실행하도록 허용하고 다른 스레드는 차단하고 대기합니다.

    많은 수의 스레드가 동일한 키로 캐시된 값을 얻으면 하나의 스레드만 로드 메소드에 들어가고 다른 스레드는 캐시된 값이 생성될 때까지 기다립니다. 이는 또한 캐시 고장의 위험을 방지합니다. 동시성이 높은 시나리오에서는 여전히 많은 수의 스레드가 차단됩니다.

  2. 새로 고침 전략 : refreshAfterWrite 하나의 스레드가 로드 메서드를 실행하고 다른 스레드가 이전 값을 반환하도록 허용합니다.

    단일 키 동시성에서는 RefreshAfterWrite를 사용하면 차단되지 않지만, 여러 키가 동시에 만료되는 경우 데이터베이스에 여전히 압력이 가해집니다.

시스템 성능을 향상시키기 위해 다음 두 가지 측면에서 최적화할 수 있습니다.

  1. 많은 수의 스레드를 차단할 가능성을 줄이려면 새로 고침 < 만료를 구성하세요.

  2. 비동기식 새로 고침 전략을 채택하십시오 . 즉, 스레드가 데이터를 비동기식으로 로드하는 동안 모든 요청은 이전 캐시 값을 반환하여 캐시 사태를 방지합니다.

아래 그림은 최적화 계획의 타임라인을 보여줍니다.

3. 비동기식 새로 고침을 구현하는 두 가지 방법

3.1 다시 로드 방법 재정의

ExecutorService executorService = Executors.newFixedThreadPool(5);
        CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                //从数据库加载
                return "value_" + key.toLowerCase();
            }

            @Override
            public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
                ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
                    System.out.println(Thread.currentThread().getName() + "异步加载 key" + key);
                    return load(key);
                });
                executorService.submit(futureTask);
                return futureTask;
            }
        };
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                //最大容量为20(基于容量进行回收)
                .maximumSize(20)
                //配置写入后多久使缓存过期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //配置写入后多久刷新缓存
                .refreshAfterWrite(1, TimeUnit.SECONDS)
                .build(cacheLoader);

3.2 asyncReloading 메소드 구현

ExecutorService executorService = Executors.newFixedThreadPool(5);

        CacheLoader.asyncReloading(
                new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        System.out.println(Thread.currentThread().getName() + "加载 key" + key);
                        //从数据库加载
                        return "value_" + key.toLowerCase();
                    }
                }
                , executorService);

4. 비동기식 새로 고침 + 다단계 캐시

장면 :

전자상거래 회사는 앱 홈페이지 인터페이스의 성능을 최적화해야 합니다. 작성자가 2레벨 캐시 모드와 Guava의 비동기 새로 고침 메커니즘을 사용하여 전체 솔루션을 완료하는 데 약 이틀이 걸렸습니다.

전체 아키텍처는 아래 그림에 나와 있습니다.

캐시 읽기 프로세스는 다음과 같습니다 .

1. 비즈니스 게이트웨이가 막 시작되면 로컬 캐시에 데이터가 없으므로 Redis 캐시를 읽어보고, Redis 캐시에 데이터가 없으면 RPC를 통해 쇼핑 가이드 서비스를 호출하여 데이터를 읽은 후 쓰기를 수행한다. 데이터를 로컬 캐시 및 Redis에 저장하고, Redis 캐시가 비어 있지 않으면 캐시된 데이터가 로컬 캐시에 기록됩니다.

2. 1단계에서 로컬 캐시가 준비되었으므로 후속 요청에서는 로컬 캐시를 직접 읽고 이를 사용자에게 반환합니다.

3. Guava는 쇼핑 가이드 서비스의 데이터를 로컬 캐시 및 Redis로 동기화하기 위해 가끔씩 사용자 정의 LoadingCache 스레드 풀(최대 스레드 5개, 코어 스레드 5개)을 호출하는 새로 고침 메커니즘으로 구성됩니다.

최적화 후 성능은 매우 좋고, 평균 소요 시간은 5ms 정도이며, GC 적용 빈도가 크게 감소합니다.

이 솔루션에는 여전히 결함이 있습니다. 어느 날 밤 우리는 앱 홈 페이지에 표시되는 데이터가 때로는 동일하고 때로는 다른 것을 발견했습니다.

즉, LoadingCache 스레드가 캐시 정보를 업데이트하기 위해 인터페이스를 호출했지만 각 서버의 로컬 캐시에 있는 데이터는 완전히 일치하지 않습니다.

이는 두 가지 매우 중요한 점을 보여줍니다.

1. 지연 로딩으로 인해 여러 시스템에서 데이터 불일치가 발생할 수 있습니다.

2. LoadingCache 스레드 풀의 수가 합리적으로 구성되지 않아 작업이 쌓입니다.

제안된 해결책은 다음과 같습니다 .

1. 비동기식 새로 고침은 메시지 메커니즘을 결합하여 캐시 데이터를 업데이트합니다. 즉, 쇼핑 가이드 서비스의 구성이 변경되면 비즈니스 게이트웨이에 데이터를 다시 가져오고 캐시를 업데이트하라는 알림이 전달됩니다.

2. LoadingCache의 스레드 풀 매개변수를 적절하게 늘리고 스레드 풀의 사용량을 모니터링하기 위해 스레드 풀에 지점을 묻어 스레드가 사용 중일 때 경보가 발생하고 스레드 풀 매개변수를 동적으로 수정할 수 있습니다.

5. 요약

Guava Cache는 매우 강력하며, load나 reload 메소드를 비동기적으로 실행하는 백그라운드 작업 스레드가 없고, 대신 요청 스레드를 통해 관련 작업을 수행합니다.

시스템 성능을 향상시키기 위해 다음 두 가지 측면에서 이를 처리할 수 있습니다.

  1. 많은 수의 스레드를 차단할 가능성을 줄이려면 새로 고침 < 만료를 구성하세요.

  2. 비동기 새로 고침 전략을 채택합니다 . 즉, 스레드가 데이터를 비동기적으로 로드하고 그 동안 모든 요청은 이전에 캐시된 값을 반환합니다 .

그럼에도 불구하고 이 접근 방식을 사용할 때는 여전히 캐시 및 데이터베이스 일관성 문제를 고려해야 합니다. 

추천

출처blog.csdn.net/qq_63815371/article/details/135428100