캐 시 는 응용 프로그램 에서 없어 서 는 안 될 것 입 니 다. 예 를 들 어 redis, memcache, 메모리 캐 시 등 을 자주 사용 합 니 다.Guava 는 Google 에서 만 든 도구 패키지 입 니 다. 그 안에 있 는 cache 는 로 컬 메모리 캐 시 에 대한 구현 입 니 다. 다양한 캐 시 만 료 정책 을 지원 합 니 다.Guava cache 의 캐 시 로드 방식 은 두 가지 가 있 습 니 다.
- CacheLoader
- Callable callback
구체 적 인 두 가지 방식 의 소 개 는 공식 문 서 를 보십시오.
http://ifeve.com/google-guava-cachesexplained/다음은 흔히 볼 수 있 는 사용법 을 살 펴 보 자.뒤의 예제 실천 은 모두 CacheLoader 방식 으로 캐 시 값 을 불 러 옵 니 다.
1. 간단 한 사용: 시한 부 만 료
LoadingCache caches = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader() {
@Override
public Object load(String key) throws Exception {
return generateValueByKey(key);
}
});
try {
System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
e.printStackTrace();
}
코드 에서 보 듯 이 caches 라 는 캐 시 대상 을 새로 만 들 었 습 니 다. maximumSize 는 캐 시 용량 크기 를 정의 합 니 다. 캐 시 수량 이 용량 이 출시 될 때 캐 시 를 회수 하고 최근 에 사용 하지 않 았 거나 전체적으로 사용 하지 않 은 캐 시 항목 을 회수 합 니 다.주의해 야 할 것 은 이 용량 상한 선 에 가 까 울 때 발생 하기 때문에 이 값 을 정의 할 때 상황 에 따라 적당히 늘 려 야 한다.또한 expireAfterWrite 라 는 방법 을 통 해 캐 시 만 료 시간 을 정의 하고 10 분 후에 만 료 됩 니 다.build 방법 에 CacheLoader 대상 이 들 어 와 load 방법 을 다시 썼 습 니 다.가 져 온 캐 시 값 이 존재 하지 않 거나 만 료 되 었 을 때 이 load 방법 을 사용 하여 캐 시 값 을 계산 합 니 다.이것 이 바로 가장 간단 하고 우리 가 평소에 가장 자주 사용 하 는 사용 방법 이다.캐 시 크기, 만 료 시간 및 캐 시 값 생 성 방법 을 정의 합 니 다.
만약 에 redis 와 같은 다른 캐 시 방식 을 사용한다 면 우 리 는 위 에 있 는 '캐 시 가 있 으 면 되 돌아 갑 니 다. 그렇지 않 으 면 연산, 캐 시, 그리고 되 돌아 갑 니 다' 의 캐 시 모드 가 매우 큰 단점 이 있다 는 것 을 알 고 있 습 니 다.높 은 동시 다발 조건 에서 get 작업 을 동시에 진행 할 때 캐 시 값 이 만 료 되 었 을 때 대량의 스 레 드 가 캐 시 값 을 만 드 는 방법 을 호출 할 수 있 습 니 다. 예 를 들 어 데이터 베이스 에서 읽 는 것 입 니 다.이 럴 때 대량의 요청 을 하면 서 데이터베이스 에 있 는 이 기록, 즉 '캐 시 뚫 기' 를 조회 하기 쉽다.(예전 에 '관통' 과 '눈사태' 라 고 불 렀 는데 개념 이해 오류 에 속 하고 댓 글 수정 에 감 사 드 립 니 다) 과 바 캐 치 는 이런 상황 을 어느 정도 통제 하고 있 습 니 다.대량의 스 레 드 가 같은 key 로 캐 시 값 을 가 져 올 때 하나의 스 레 드 만 load 방법 에 들 어가 고 다른 스 레 드 는 캐 시 값 이 생 성 될 때 까지 기 다 립 니 다.이렇게 하면 캐 시가 뚫 릴 위험 을 피 할 수 있다.
2. 진급 사용: 정시 갱신
위의 사용 방법 과 같이 캐 시 를 뚫 는 경 우 는 없 지만 캐 시 값 이 만 료 될 때마다 대량의 요청 스 레 드 가 막 힐 수 있 습 니 다.한편, Guava 는 다른 캐 시 정책 을 제공 합 니 다. 캐 시 값 을 정기 적 으로 갱신 합 니 다. 스 레 드 호출 load 방법 으로 캐 시 를 업데이트 하고 다른 요청 스 레 드 는 이 캐 시 의 이전 값 을 되 돌려 줍 니 다.이렇게 하면 어떤 key 의 캐 시 에 있어 서 하나의 스 레 드 만 막 혀 캐 시 값 을 만 들 고 다른 스 레 드 는 오래된 캐 시 값 으로 돌아 가 막 히 지 않 습 니 다.Guava cache 의 refresh After Write 방법 이 필요 합 니 다.다음 과 같다.
LoadingCache caches = CacheBuilder.newBuilder()
.maximumSize(100)
.refreshAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader() {
@Override
public Object load(String key) throws Exception {
return generateValueByKey(key);
}
});
try {
System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
e.printStackTrace();
}
코드 에서 보 듯 이 10 분 간격 으로 캐 시 값 이 갱 신 됩 니 다.
그리고 주의해 야 할 점 은 이곳 의 시간 은 진정한 의미 의 시간 이 아니다.Guava cache 의 리 셋 은 사용자 가 스 레 드 를 요청 하여 이 스 레 드 를 load 방법 으로 호출 해 야 하기 때문에 사용자 가 이 캐 시 값 을 가 져 오 려 고 시도 하지 않 으 면 이 캐 시 는 리 셋 되 지 않 습 니 다.
3. 진급 사용: 비동기 리 셋
2 의 사용 방법 과 같이 같은 키 의 캐 시가 만 료 되면 여러 개의 스 레 드 가 막 히 는 문 제 를 해결 하고 캐 시 새로 고침 작업 을 수행 하 는 한 사용자 스 레 드 만 막 힙 니 다.이 를 통 해 다른 문 제 를 생각 할 수 있 습 니 다. 캐 시 된 key 가 많 을 때 높 은 병행 조건 에서 대량의 스 레 드 가 서로 다른 key 에 대응 하 는 캐 시 를 가 져 옵 니 다. 이때 도 대량의 스 레 드 가 막 히 고 데이터 베이스 에 큰 부담 을 줄 수 있 습 니 다.이 문제 의 해결 방법 은 캐 시 값 을 새로 고 치 는 작업 을 배경 스 레 드 에 맡 기 는 것 입 니 다. 모든 사용자 요청 스 레 드 는 오래된 캐 시 값 으로 돌아 갑 니 다. 그러면 사용자 스 레 드 가 막 히 지 않 습 니 다.상세 한 방법 은 다음 과 같다.
ListeningExecutorService backgroundRefreshPools =
MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));
LoadingCache caches = CacheBuilder.newBuilder()
.maximumSize(100)
.refreshAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader() {
@Override
public Object load(String key) throws Exception {
return generateValueByKey(key);
}
@Override
public ListenableFuture reload(String key,
Object oldValue) throws Exception {
return backgroundRefreshPools.submit(new Callable() {
@Override
public Object call() throws Exception {
return generateValueByKey(key);
}
});
}
});
try {
System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
e.printStackTrace();
}
위의 코드 에서 캐 시 리 셋 작업 을 수행 하기 위해 스 레 드 탱크 를 새로 만 들 었 습 니 다.또한 CacheLoader 의 reload 방법 을 다시 썼 습 니 다. 이 방법 에서 캐 시 새로 고침 작업 을 만 들 고 스 레 드 탱크 에 제출 합 니 다.이 때 캐 시 리 셋 은 여전히 사용자 스 레 드 에 의 해 작 동 되 어야 합 니 다. 다만 2 와 다른 점 은 이 사용자 스 레 드 가 리 셋 작업 을 촉발 한 후에 바로 오래된 캐 시 값 을 되 돌려 줍 니 다.
TIPS
- 캐 시 차단 과 사용자 스 레 드 차단 은 모두 이전 값 으로 돌아 가 는 것 을 볼 수 있 습 니 다.따라서 오래된 값 이 없 으 면 모두 막 힐 수 있 으 므 로 상황 에 따라 시스템 이 시 작 될 때 캐 시 내용 을 메모리 에 불 러 와 야 합 니 다.
- 캐 시 를 새로 고 칠 때 generateValueByKey 방법 에 이상 이 생기 거나 null 로 되 돌아 오 면 이전 값 은 업데이트 되 지 않 습 니 다.
- 별말: 메모리 캐 시 를 사용 할 때 캐 시 값 을 받 은 후에 업무 코드 에서 캐 시 를 직접 수정 하지 마 십시오. 이때 받 은 대상 의 인용 은 캐 시의 진정한 내용 을 가리 키 기 때 문 입 니 다.이 대상 에서 직접 수정 이 필요 하 다 면 캐 시 값 을 가 져 온 후 복사 본 을 복사 한 후 이 복사 본 을 전달 하여 수정 작업 을 한다.(나 는 일찍이 이 저급한 잘못 을 저 지 른 적 이 있다 -!)
4. 단순 추상 패키지
다음은 Guava cache 를 기반 으로 추상 화 된 캐 시 도구 류 입 니 다.(추상 이 좋 지 않 아 겨우 쓸 수 있다 -!)개선 의견 이 있 으 면 잘 부탁드립니다.
/**
* @description: guava 。 , 。 。
* getValue , refreshDuration, refreshTimeunit, maxSize
* , 20.
* @author: luozhuo
* @date: 2017 6 21 10:03:45
* @version: V1.0.0
* @param
* @param
*/
public abstract class BaseGuavaCache {
private Logger logger = LoggerFactory.getLogger(getClass());
//
protected int refreshDuration = 10;
//
protected TimeUnit refreshTimeunit = TimeUnit.MINUTES;
// ( )
protected int expireDuration = -1;
//
protected TimeUnit expireTimeunit = TimeUnit.HOURS;
//
protected int maxSize = 4;
//
protected static ListeningExecutorService refreshPool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));
private LoadingCache cache = null;
/**
* ( , )
*/
public abstract void loadValueWhenStarted();
/**
* @description:
* @description: ,get
* @param key
* @author: luozhuo
* @throws Exception
* @date: 2017 6 14 7:11:10
*/
protected abstract V getValueWhenExpired(K key) throws Exception;
/**
* @description: cache
* @param key
* @author: luozhuo
* @throws Exception
* @date: 2017 6 13 5:07:11
*/
public V getValue(K key) throws Exception {
try {
return getCache().get(key);
} catch (Exception e) {
logger.error(" ,key: " + key, e);
throw e;
}
}
public V getValueOrDefault(K key, V defaultValue) {
try {
return getCache().get(key);
} catch (Exception e) {
logger.error(" ,key: " + key, e);
return defaultValue;
}
}
/**
*
*/
public BaseGuavaCache setRefreshDuration( int refreshDuration ){
this.refreshDuration = refreshDuration;
return this;
}
public BaseGuavaCache setRefreshTimeUnit(TimeUnit refreshTimeunit){
this.refreshTimeunit = refreshTimeunit;
return this;
}
public BaseGuavaCache setExpireDuration( int expireDuration ){
this.expireDuration = expireDuration;
return this;
}
public BaseGuavaCache setExpireTimeUnit(TimeUnit expireTimeunit){
this.expireTimeunit = expireTimeunit;
return this;
}
public BaseGuavaCache setMaxSize( int maxSize ){
this.maxSize = maxSize;
return this;
}
public void clearAll(){
this.getCache().invalidateAll();
}
/**
* @description: cache
* @author: luozhuo
* @date: 2017 6 13 2:50:11
*/
private LoadingCache getCache() {
if(cache == null){
synchronized (this) {
if(cache == null){
CacheBuilder cacheBuilder = CacheBuilder.newBuilder()
.maximumSize(maxSize);
if(refreshDuration > 0) {
cacheBuilder = cacheBuilder.refreshAfterWrite(refreshDuration, refreshTimeunit);
}
if(expireDuration > 0) {
cacheBuilder = cacheBuilder.expireAfterWrite(expireDuration, expireTimeunit);
}
cache = cacheBuilder.build(new CacheLoader() {
@Override
public V load(K key) throws Exception {
return getValueWhenExpired(key);
}
@Override
public ListenableFuture reload(final K key,
V oldValue) throws Exception {
return refreshPool.submit(new Callable() {
public V call() throws Exception {
return getValueWhenExpired(key);
}
});
}
} );
}
}
}
return cache;
}
@Override
public String toString() {
return "GuavaCache";
}
}