spring boot+spring cache 2 급 캐 시 구현(redis+caffeine)

37423 단어 springbootcache
spring boot 에 spring cache 가 통합 되 었 고 여러 가지 캐 시 방식 이 실현 되 었 습 니 다.예 를 들 어 Redis,Caffine,JCache,EhCache 등 이 있 습 니 다.그러나 하나의 캐 시 만 사용 하면 큰 네트워크 소모(예 를 들 어 Redis)가 있 거나 메모리 사용량 이 너무 많 습 니 다(예 를 들 어 Caffine 같은 메모리 캐 시).많은 장면 에서 결합 하여 1,2 급 캐 시 방식 을 실현 할 수 있 고 응용 처리 효율 을 어느 정도 향상 시 킬 수 있다.
내용 설명:
  • 캐 시,2 급 캐 시
  • spring cache:주로 spring cache 가 정의 하 는 인터페이스 방법 설명 과 주해 중의 속성 설명
  • 을 포함한다.
  • spring boot+spring cache:RedisCache 구현 중의 결함
  • caffeine 소개
  • spring boot+spring cache 는 2 급 캐 시(redis+caffeine)
  • 를 실현 합 니 다.
    캐 시
    간단 한 이해,캐 시 는 읽 기 느 린 미디어 에서 데 이 터 를 읽 어서 읽 기 빠 른 미디어 에 넣 는 것 입 니 다.예 를 들 어 디스크-->메모리.평소에 우 리 는 데이터 베 이 스 를 디스크 에 저장 합 니 다.예 를 들 어 데이터베이스.매번 데이터베이스 에서 읽 으 면 디스크 자체 의 IO 가 읽 기 속도 에 영향 을 주기 때문에 redis 와 같은 메모리 캐 시가 있 습 니 다.데 이 터 를 읽 어 메모리 에 넣 을 수 있 습 니 다.데 이 터 를 가 져 올 때 메모리 에서 데 이 터 를 직접 가 져 와 서 속 도 를 어느 정도 높 일 수 있 습 니 다.그러나 일반적으로 redis 는 단독 적 으로 클 러 스 터 로 배치 되 기 때문에 네트워크 IO 의 소모 가 있 을 수 있 습 니 다.redis 클 러 스 터 와 의 링크 는 연결 탱크 라 는 도구 가 있 지만 데이터 전송 에 도 어느 정도 소모 가 있 을 수 있 습 니 다.그래서 응용 내 캐 시 가 생 겼 습 니 다.예 를 들 어 caffeine.내 캐 시 에 조건 에 맞 는 데이터 가 있 을 때 직접 사용 할 수 있 으 며,네트워크 를 통 해 redis 에 가서 가 져 오지 않 아 도 2 급 캐 시 를 만 들 수 있 습 니 다.응용 내 캐 시 는 1 급 캐 시 라 고 하고 원 격 캐 시(예 를 들 어 redis)는 2 급 캐 시 라 고 합 니 다.
    spring cache
    캐 시 를 사용 할 때 보통 다음 과 같은 절차 입 니 다.

    프로 세 스 도 에서 알 수 있 듯 이 캐 시 를 사용 하기 위해 기 존의 업무 처 리 를 바탕 으로 캐 시 에 대한 조작 을 많이 추 가 했 습 니 다.이 를 업무 코드 에 결합 시 키 면 중복 적 인 작업 이 많 고 코드 에 따라 업 무 를 이해 하 는 데 불리 합 니 다.
    spring cache 는 spring-context 패키지 에서 제공 하 는 주석 기반 캐 시 구성 요소 로 표준 인 터 페 이 스 를 정의 합 니 다.이 인 터 페 이 스 를 실현 하면 방법 적 으로 주 해 를 추가 하여 캐 시 를 실현 할 수 있 습 니 다.이렇게 하면 캐 시 코드 와 업무 처리 가 결 합 된 문 제 를 피 할 수 있다.spring cache 의 실현 은 spring aop 에서 방법 절단면(MethodInterceptor)을 사용 하여 패 키 징 한 확장 입 니 다.물론 spring aop 도 Aspect 를 바탕 으로 이 루어 집 니 다.
    spring cache 핵심 인 터 페 이 스 는 두 개 입 니 다.Cache 와 Cache Manager 입 니 다.

    캐 시 인터페이스
    캐 시 를 제공 하 는 구체 적 인 작업,예 를 들 어 캐 시 를 넣 고 읽 고 청소 하 는 것,spring 프레임 워 크 에서 기본적으로 제공 하 는 것 은 다음 과 같 습 니 다.

    RedisCache 를 제외 하고 spring-data-redis 패키지 에 있 습 니 다.다른 기본 은 spring-context-support 패키지 에 있 습 니 다.
    
    #Cache.java
    
    package org.springframework.cache;
    
    import java.util.concurrent.Callable;
    
    public interface Cache {
    
     // cacheName,     ,        CacheManager  Cache bean   cacheName
     String getName();
    
     //          , :RedisTemplate、com.github.benmanes.caffeine.cache.Cache<Object, Object>。         ,             bean,                    
     Object getNativeCache();
    
     //   key     ,      ValueWrapper,           ,         ,  get       
     ValueWrapper get(Object key);
    
     //   key     ,       ,         
     <T> T get(Object key, Class<T> type);
    
     //   key     ,    valueLoader.call()    @Cacheable     。 @Cacheable   sync     true      。                   。                  
     <T> T get(Object key, Callable<T> valueLoader);
    
     //  @Cacheable              
     void put(Object key, Object value);
    
     //        key      。     key        
     ValueWrapper putIfAbsent(Object key, Object value);
    
     //     
     void evict(Object key);
    
     //           。      ,          @Cacheable         ,            
     void clear();
    
     //         
     interface ValueWrapper {
    
     //          
     Object get();
     }
    
     //  {@link #get(Object, Callable)}     ,         
     @SuppressWarnings("serial")
     class ValueRetrievalException extends RuntimeException {
    
     private final Object key;
    
     public ValueRetrievalException(Object key, Callable<?> loader, Throwable ex) {
      super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex);
      this.key = key;
     }
    
     public Object getKey() {
      return this.key;
     }
     }
    }
    
    
    CacheManager 인터페이스
    주로 Cache 를 제공 하여 bean 의 생 성 을 실현 합 니 다.모든 응용 프로그램 에서 cacheName 을 통 해 Cache 를 격 리 할 수 있 고 모든 cacheName 은 하나의 Cache 에 대응 합 니 다.spring 프레임 워 크 에서 기본적으로 제공 하 는 실현 과 Cache 의 실현 은 모두 쌍 을 이 루어 나타 나 고 가방 구조 도 위의 그림 에 있 습 니 다.
    
    #CacheManager.java
    
    package org.springframework.cache;
    
    import java.util.Collection;
    
    public interface CacheManager {
    
     //   cacheName  Cache   bean,             Cache  bean,      ,         ( Caffeine)                
     Cache getCache(String name);
    
     //      cacheName
     Collection<String> getCacheNames();
    }
    상용 주해 설명
    @Cacheable:데 이 터 를 조회 하 는 방법 에 주로 사 용 됩 니 다.
    
    package org.springframework.cache.annotation;
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Inherited;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    import java.util.concurrent.Callable;
    import org.springframework.core.annotation.AliasFor;
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface Cacheable {
        // cacheNames,CacheManager             Cache  bean
     @AliasFor("cacheNames")
     String[] value() default {};
    
     @AliasFor("value")
     String[] cacheNames() default {};
    
        //    key,  SpEL   。              hashCode      (SimpleKey)
     String key() default "";
    
     //   key   ,     SimpleKeyGenerator
     String keyGenerator() default "";
    
     //       CacheManager
     String cacheManager() default "";
    
     //      
     String cacheResolver() default "";
    
     //      ,  SpEL   ,              。           
     String condition() default "";
        
        //           ,  SpEL   ,         
     String unless() default "";
    
     //             ,       ,   false,    Cache.get(key)  ;   true,    Cache.get(key, Callable)  
     boolean sync() default false;
    }
    
    
    @CacheEvict:캐 시 를 지우 고 데 이 터 를 삭제 하 는 방법 에 주로 사 용 됩 니 다.Cacheable 보다 속성 이 두 개 더 많아 졌어 요.
    
    package org.springframework.cache.annotation;
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Inherited;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    import org.springframework.core.annotation.AliasFor;
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface CacheEvict {
        // ...         @Cacheable    
    
     //             , false     Cache.evict(key)  ; true     Cache.clear()  
     boolean allEntries() default false;
    
     //              
     boolean beforeInvocation() default false;
    }
    
    
  • @CachePut:캐 시 를 넣 고 데 이 터 를 업데이트 하 는 방법 에 주로 사 용 됩 니 다.속성 설명 참조@Cacheable
  • @Caching:한 방법 에 여러 주 해 를 설정 하 는 데 사용 합 니 다
  • @EnableCashing:spring cache 캐 시 를 사용 합 니 다.전체 스위치 로 spring boot 의 시작 클래스 나 설정 클래스 에 이 주 해 를 추가 해 야 유효 합 니 다
  • spring boot + spring cache
    spring boot 에 서 는 spring cache 를 통합 하고 다양한 캐 시 설정 을 제공 합 니 다.사용 할 때 어떤 캐 시(enum CacheType)를 사용 하 는 지 설정 하면 됩 니 다.

    spring boot 에 확장 할 수 있 는 것 이 하나 더 추가 되 었 습 니 다.바로 CacheManager Customizer 인터페이스 입 니 다.이 인 터 페 이 스 를 사용자 정의 로 실현 한 다음 에 CacheManager 에 대해 설정 할 수 있 습 니 다.예 를 들 어:
    
    package com.itopener.demo.cache.redis.config;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
    import org.springframework.data.redis.cache.RedisCacheManager;
    
    public class RedisCacheManagerCustomizer implements CacheManagerCustomizer<RedisCacheManager> {
    
     @Override
     public void customize(RedisCacheManager cacheManager) {
     //       ,   
     cacheManager.setDefaultExpiration(1000);
     cacheManager.setUsePrefix(false);
     Map<String, Long> expires = new ConcurrentHashMap<String, Long>();
     expires.put("userIdCache", 2000L);
     cacheManager.setExpires(expires);
     }
    
    }
    
    
    이 bean 불 러 오기:
    
    package com.itopener.demo.cache.redis.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author fuwei.deng
     * @date 2017 12 22    10:24:54
     * @version 1.0.0
     */
    @Configuration
    public class CacheRedisConfiguration {
     
     @Bean
     public RedisCacheManagerCustomizer redisCacheManagerCustomizer() {
     return new RedisCacheManagerCustomizer();
     }
    }
    
    
    자주 사용 하 는 캐 시 는 Redis 입 니 다.Redis 는 spring cache 인터페이스 에 대한 구현 은 spring-data-redis 패키지 에 있 습 니 다.

    여기 서 제 가 생각 하 는 RedisCache 실현 중의 결함 을 말씀 드 리 겠 습 니 다.
    1.캐 시가 효력 을 잃 는 순간 캐 시 데 이 터 를 가 져 오 는 스 레 드 가 있 으 면 null 로 되 돌아 갈 수 있 습 니 다.이 유 는 RedisCache 가 다음 과 같은 절 차 를 밟 았 기 때 문 입 니 다.
  • 캐 시 키 가 존재 하 는 지 판단 하기
  • 키 가 존재 하면 캐 시 데 이 터 를 가 져 오고 되 돌려 줍 니 다
  • 따라서 키 가 존재 하 는 것 을 판단 한 후 캐 시가 효력 을 잃 었 다 고 판단 하고 캐 시가 데이터 가 없 으 면 null 로 돌아 갑 니 다.
    2.RedisCacheManager 에서 빈 값 의 속성(cacheNullValues)을 기본적으로 false 로 저장 할 수 있 는 지 여부 입 니 다.즉,빈 값 을 저장 할 수 없습니다.그러면 캐 시 관통 위험 이 존재 합 니 다.결함 은 이 속성 이 final 형식 입 니 다.생 성 대상 은 구조 적 방법 으로 만 들 어 갈 수 있 기 때문에 캐 시 관통 을 피 하려 면 응용 프로그램 에서 RedisCacheManager 라 는 bean 을 설명 할 수 밖 에 없습니다.
    3.RedisCacheManager 의 속성 은 설정 파일 을 통 해 직접 설정 할 수 없고 응용 프로그램 에서 CacheManager Customizer 인터페이스 만 설정 할 수 있 습 니 다.개인 적 으로 불편 합 니 다.
    Caffeine
    Caffine 은 구 글 의 오픈 소스 인 Guava 디자인 이념 을 바탕 으로 하 는 고성능 메모리 캐 시 로 자바 8 을 사용 하여 개발 되 었 으 며,spring boot 는 Caffine 을 도입 한 후 점차 Guava 의 통합 을 폐기 하 였 다.Caffine 소스 코드 및 소개 주소:caffeine
    caffeine 은 다양한 캐 시 충전 전략,값 회수 전략 을 제공 하 는 동시에 캐 시 명중 횟수 등 통계 데 이 터 를 포함 하여 캐 시 최적화 에 큰 도움 을 줄 수 있 습 니 다.
    caffeine 의 소 개 는 참고 할 수 있 습 니 다.https://www.jb51.net/article/134242.htm
    간단하게 말하자면 caffeine 의 시간 기반 회수 전략 은 다음 과 같은 몇 가지 가 있다.
  • expireAfterAccess:방문 후 만 료 되 며,지난번 읽 거나 쓰기 발생 후 만 료 시간
  • expireAfterWrite:기록 후 만 료 되 며,지난번 기록 발생 후 만 료 시간
  • 사용자 정의 정책:만 료 시간 은 Expiry 인 터 페 이 스 를 실현 한 후 단독으로 계산 합 니 다
  • spring boot+spring cache 2 급 캐 시 구현(redis+caffeine)
    본인 이 처음에 언급 했 듯 이 redis 캐 시 를 사용 하 더 라 도 어느 정도 네트워크 전송 에 소모 가 있 을 수 있 습 니 다.실제 응용 에서 변경 빈도 가 매우 낮은 데이터 가 존재 하면 응용 내부 에 직접 캐 시 할 수 있 고 실시 간 요구 가 높 지 않 은 데이터 에 대해 내부 캐 시 를 일정 시간 사용 하여 redis 에 대한 방문 을 줄 일 수 있 습 니 다.응답 속도 향상
    spring-data-redis 프레임 워 크 에서 redis 는 spring cache 의 실현 에 부족 하기 때문에 사용 할 때 문제 가 발생 할 수 있 기 때문에 원래 의 실현 을 바탕 으로 확장 하지 않 고 실현 방식 을 직접 참고 하여 Cache 와 CacheManager 인 터 페 이 스 를 실현 합 니 다.
    또한 주의해 야 할 것 은 일반 응용 프로그램 에 여러 노드 가 배치 되 어 있 고 1 급 캐 시 는 응용 프로그램 에 있 는 캐 시 이기 때문에 데이터 업데이트 와 제거 시 모든 노드 에 캐 시 를 정리 하 는 작업 을 알려 야 합 니 다.이러한 효 과 를 실현 할 수 있 는 여러 가지 방법 이 있 습 니 다.예 를 들 어 zookeeper,MQ 등 입 니 다.그러나 redis 캐 시 를 사 용 했 기 때문에 redis 자체 가 구독/발표 기능 을 지원 하기 때문에 다른 구성 요소 에 의존 하지 않 고 redis 채널 을 사용 하여 다른 노드 에 캐 시 를 청소 하 는 작업 을 알 립 니 다.
    다음은 spring boot+spring cache 에 대해 2 급 캐 시(redis+caffeine)를 실현 하 는 starter 패키지 절차 와 소스 코드 입 니 다.
    properties 설정 속성 클래스 정의
    
    package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    /** 
     * @author fuwei.deng
     * @date 2018 1 29    11:32:15
     * @version 1.0.0
     */
    @ConfigurationProperties(prefix = "spring.cache.multi")
    public class CacheRedisCaffeineProperties { 
     private Set<String> cacheNames = new HashSet<>(); 
     /**       ,  true,      */
     private boolean cacheNullValues = true; 
     /**       cacheName  Cache   ,  true*/
     private boolean dynamic = true;
     
     /**   key   */
     private String cachePrefix; 
     private Redis redis = new Redis(); 
     private Caffeine caffeine = new Caffeine();
     public class Redis { 
     /**       ,    ,     */
     private long defaultExpiration = 0;
     
     /**   cacheName     ,    ,    defaultExpiration */
     private Map<String, Long> expires = new HashMap<>();
     
     /**             topic  */
     private String topic = "cache:redis:caffeine:topic";
    
     public long getDefaultExpiration() {
      return defaultExpiration;
     }
    
     public void setDefaultExpiration(long defaultExpiration) {
      this.defaultExpiration = defaultExpiration;
     }
    
     public Map<String, Long> getExpires() {
      return expires;
     }
    
     public void setExpires(Map<String, Long> expires) {
      this.expires = expires;
     }
    
     public String getTopic() {
      return topic;
     }
    
     public void setTopic(String topic) {
      this.topic = topic;
     }
     
     }
     
     public class Caffeine { 
     /**        ,    */
     private long expireAfterAccess;
     
     /**        ,    */
     private long expireAfterWrite;
     
     /**        ,    */
     private long refreshAfterWrite;
     
     /**      */
     private int initialCapacity;
     
     /**         ,                */
     private long maximumSize;
     
     /**              ,    spring cache         ,        */
    // private long maximumWeight;
     
     public long getExpireAfterAccess() {
      return expireAfterAccess;
     }
    
     public void setExpireAfterAccess(long expireAfterAccess) {
      this.expireAfterAccess = expireAfterAccess;
     }
    
     public long getExpireAfterWrite() {
      return expireAfterWrite;
     }
    
     public void setExpireAfterWrite(long expireAfterWrite) {
      this.expireAfterWrite = expireAfterWrite;
     }
    
     public long getRefreshAfterWrite() {
      return refreshAfterWrite;
     }
    
     public void setRefreshAfterWrite(long refreshAfterWrite) {
      this.refreshAfterWrite = refreshAfterWrite;
     }
    
     public int getInitialCapacity() {
      return initialCapacity;
     }
    
     public void setInitialCapacity(int initialCapacity) {
      this.initialCapacity = initialCapacity;
     }
    
     public long getMaximumSize() {
      return maximumSize;
     }
    
     public void setMaximumSize(long maximumSize) {
      this.maximumSize = maximumSize;
     }
     }
    
     public Set<String> getCacheNames() {
     return cacheNames;
     }
    
     public void setCacheNames(Set<String> cacheNames) {
     this.cacheNames = cacheNames;
     }
    
     public boolean isCacheNullValues() {
     return cacheNullValues;
     }
    
     public void setCacheNullValues(boolean cacheNullValues) {
     this.cacheNullValues = cacheNullValues;
     }
    
     public boolean isDynamic() {
     return dynamic;
     }
    
     public void setDynamic(boolean dynamic) {
     this.dynamic = dynamic;
     }
    
     public String getCachePrefix() {
     return cachePrefix;
     }
    
     public void setCachePrefix(String cachePrefix) {
     this.cachePrefix = cachePrefix;
     }
    
     public Redis getRedis() {
     return redis;
     }
    
     public void setRedis(Redis redis) {
     this.redis = redis;
     }
    
     public Caffeine getCaffeine() {
     return caffeine;
     }
    
     public void setCaffeine(Caffeine caffeine) {
     this.caffeine = caffeine;
     }
    }
    spring cache 에는 Cache 인 터 페 이 스 를 실현 하 는 추상 적 인 클래스 AbstractValue Adapting Cache 가 있 습 니 다.빈 값 의 포장 과 캐 시 값 의 포장 을 포함 하기 때문에 Cache 인 터 페 이 스 를 실현 하지 않 고 AbstractValue Adapting Cache 추상 류 를 직접 실현 합 니 다.
    
    package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
    import java.lang.reflect.Constructor;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.Callable;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.ReentrantLock;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.cache.support.AbstractValueAdaptingCache;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.util.StringUtils;
    import com.github.benmanes.caffeine.cache.Cache;
    import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;
    
    /**
     * @author fuwei.deng
     * @date 2018 1 26    5:24:11
     * @version 1.0.0
     */
    public class RedisCaffeineCache extends AbstractValueAdaptingCache { 
     private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class);
     private String name;
     private RedisTemplate<Object, Object> redisTemplate;
     private Cache<Object, Object> caffeineCache;
     private String cachePrefix;
     private long defaultExpiration = 0;
     private Map<String, Long> expires;
     private String topic = "cache:redis:caffeine:topic"; 
     protected RedisCaffeineCache(boolean allowNullValues) {
     super(allowNullValues);
     }
     
     public RedisCaffeineCache(String name, RedisTemplate<Object, Object> redisTemplate, Cache<Object, Object> caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) {
     super(cacheRedisCaffeineProperties.isCacheNullValues());
     this.name = name;
     this.redisTemplate = redisTemplate;
     this.caffeineCache = caffeineCache;
     this.cachePrefix = cacheRedisCaffeineProperties.getCachePrefix();
     this.defaultExpiration = cacheRedisCaffeineProperties.getRedis().getDefaultExpiration();
     this.expires = cacheRedisCaffeineProperties.getRedis().getExpires();
     this.topic = cacheRedisCaffeineProperties.getRedis().getTopic();
     }
    
     @Override
     public String getName() {
     return this.name;
     }
    
     @Override
     public Object getNativeCache() {
     return this;
     }
    
     @SuppressWarnings("unchecked")
     @Override
     public <T> T get(Object key, Callable<T> valueLoader) {
     Object value = lookup(key);
     if(value != null) {
      return (T) value;
     }
     
     ReentrantLock lock = new ReentrantLock();
     try {
      lock.lock();
      value = lookup(key);
      if(value != null) {
      return (T) value;
      }
      value = valueLoader.call();
      Object storeValue = toStoreValue(valueLoader.call());
      put(key, storeValue);
      return (T) value;
     } catch (Exception e) {
      try {
            Class<?> c = Class.forName("org.springframework.cache.Cache$ValueRetrievalException");
            Constructor<?> constructor = c.getConstructor(Object.class, Callable.class, Throwable.class);
            RuntimeException exception = (RuntimeException) constructor.newInstance(key, valueLoader, e.getCause());
            throw exception;        
          } catch (Exception e1) {
            throw new IllegalStateException(e1);
          }
     } finally {
      lock.unlock();
     }
     }
    
     @Override
     public void put(Object key, Object value) {
     if (!super.isAllowNullValues() && value == null) {
      this.evict(key);
          return;
        }
     long expire = getExpire();
     if(expire > 0) {
      redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
     } else {
      redisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
     }
     
     push(new CacheMessage(this.name, key));
     
     caffeineCache.put(key, value);
     }
    
     @Override
     public ValueWrapper putIfAbsent(Object key, Object value) {
     Object cacheKey = getKey(key);
     Object prevValue = null;
     //         ,   redis setIfAbsent       
     synchronized (key) {
      prevValue = redisTemplate.opsForValue().get(cacheKey);
      if(prevValue == null) {
      long expire = getExpire();
      if(expire > 0) {
       redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
      } else {
       redisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
      }
      
      push(new CacheMessage(this.name, key));
      
      caffeineCache.put(key, toStoreValue(value));
      }
     }
     return toValueWrapper(prevValue);
     }
    
     @Override
     public void evict(Object key) {
     //    redis     ,    caffeine    ,           caffeine          redis    caffeine 
     redisTemplate.delete(getKey(key));
     
     push(new CacheMessage(this.name, key));
     
     caffeineCache.invalidate(key);
     }
    
     @Override
     public void clear() {
     //    redis     ,    caffeine    ,           caffeine          redis    caffeine 
     Set<Object> keys = redisTemplate.keys(this.name.concat(":"));
     for(Object key : keys) {
      redisTemplate.delete(key);
     }
     
     push(new CacheMessage(this.name, null));
     
     caffeineCache.invalidateAll();
     }
    
     @Override
     protected Object lookup(Object key) {
     Object cacheKey = getKey(key);
     Object value = caffeineCache.getIfPresent(key);
     if(value != null) {
      logger.debug("get cache from caffeine, the key is : {}", cacheKey);
      return value;
     }
     
     value = redisTemplate.opsForValue().get(cacheKey);
     
     if(value != null) {
      logger.debug("get cache from redis and put in caffeine, the key is : {}", cacheKey);
      caffeineCache.put(key, value);
     }
     return value;
     }
    
     private Object getKey(Object key) {
     return this.name.concat(":").concat(StringUtils.isEmpty(cachePrefix) ? key.toString() : cachePrefix.concat(":").concat(key.toString()));
     }
     
     private long getExpire() {
     long expire = defaultExpiration;
     Long cacheNameExpire = expires.get(this.name);
     return cacheNameExpire == null ? expire : cacheNameExpire.longValue();
     }
     
     /**
     * @description                  
     * @author fuwei.deng
     * @date 2018 1 31    3:20:28
     * @version 1.0.0
     * @param message
     */
     private void push(CacheMessage message) {
     redisTemplate.convertAndSend(topic, message);
     }
     
     /**
     * @description       
     * @author fuwei.deng
     * @date 2018 1 31    3:15:39
     * @version 1.0.0
     * @param key
     */
     public void clearLocal(Object key) {
     logger.debug("clear local cache, the key is : {}", key);
     if(key == null) {
      caffeineCache.invalidateAll();
     } else {
      caffeineCache.invalidate(key);
     }
     }
    }
    CacheManager 인터페이스 구현
    
    package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
    
    import java.util.Collection;
    import java.util.Set;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ConcurrentMap;
    import java.util.concurrent.TimeUnit;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.cache.Cache;
    import org.springframework.cache.CacheManager;
    import org.springframework.data.redis.core.RedisTemplate;
    
    import com.github.benmanes.caffeine.cache.Caffeine;
    import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;
    
    /**
     * @author fuwei.deng
     * @date 2018 1 26    5:24:52
     * @version 1.0.0
     */
    public class RedisCaffeineCacheManager implements CacheManager {
     
     private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class);
     
     private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>();
     
     private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
     
     private RedisTemplate<Object, Object> redisTemplate;
    
     private boolean dynamic = true;
    
     private Set<String> cacheNames;
    
     public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties,
      RedisTemplate<Object, Object> redisTemplate) {
     super();
     this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties;
     this.redisTemplate = redisTemplate;
     this.dynamic = cacheRedisCaffeineProperties.isDynamic();
     this.cacheNames = cacheRedisCaffeineProperties.getCacheNames();
     }
    
     @Override
     public Cache getCache(String name) {
     Cache cache = cacheMap.get(name);
     if(cache != null) {
      return cache;
     }
     if(!dynamic && !cacheNames.contains(name)) {
      return cache;
     }
     
     cache = new RedisCaffeineCache(name, redisTemplate, caffeineCache(), cacheRedisCaffeineProperties);
     Cache oldCache = cacheMap.putIfAbsent(name, cache);
     logger.debug("create cache instance, the cache name is : {}", name);
     return oldCache == null ? cache : oldCache;
     }
     
     public com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache(){
     Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
     if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess() > 0) {
      cacheBuilder.expireAfterAccess(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess(), TimeUnit.MILLISECONDS);
     }
     if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite() > 0) {
      cacheBuilder.expireAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite(), TimeUnit.MILLISECONDS);
     }
     if(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity() > 0) {
      cacheBuilder.initialCapacity(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity());
     }
     if(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize() > 0) {
      cacheBuilder.maximumSize(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize());
     }
     if(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite() > 0) {
      cacheBuilder.refreshAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite(), TimeUnit.MILLISECONDS);
     }
     return cacheBuilder.build();
     }
    
     @Override
     public Collection<String> getCacheNames() {
     return this.cacheNames;
     }
     
     public void clearLocal(String cacheName, Object key) {
     Cache cache = cacheMap.get(cacheName);
     if(cache == null) {
      return ;
     }
     
     RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache;
     redisCaffeineCache.clearLocal(key);
     }
    }
    
    
    redis 메시지 발표/구독,전송 메시지 클래스
    
    package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
    import java.io.Serializable;
    
    /** 
     * @author fuwei.deng
     * @date 2018 1 29    1:31:17
     * @version 1.0.0
     */
    public class CacheMessage implements Serializable {
    
     /** */
     private static final long serialVersionUID = 5987219310442078193L;
    
     private String cacheName; 
     private Object key;
     public CacheMessage(String cacheName, Object key) {
     super();
     this.cacheName = cacheName;
     this.key = key;
     }
    
     public String getCacheName() {
     return cacheName;
     }
    
     public void setCacheName(String cacheName) {
     this.cacheName = cacheName;
     }
    
     public Object getKey() {
     return key;
     }
    
     public void setKey(Object key) {
     this.key = key;
     }
    }
    redis 메 시 지 를 감청 하려 면 MessageListener 인터페이스 가 필요 합 니 다.
    
    package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.data.redis.connection.Message;
    import org.springframework.data.redis.connection.MessageListener;
    import org.springframework.data.redis.core.RedisTemplate;
    /** 
     * @author fuwei.deng
     * @date 2018 1 30    5:22:33
     * @version 1.0.0
     */
    public class CacheMessageListener implements MessageListener { 
     private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class);
     private RedisTemplate<Object, Object> redisTemplate;
     private RedisCaffeineCacheManager redisCaffeineCacheManager;
     public CacheMessageListener(RedisTemplate<Object, Object> redisTemplate,
      RedisCaffeineCacheManager redisCaffeineCacheManager) {
     super();
     this.redisTemplate = redisTemplate;
     this.redisCaffeineCacheManager = redisCaffeineCacheManager;
     }
    
     @Override
     public void onMessage(Message message, byte[] pattern) {
     CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
     logger.debug("recevice a redis topic message, clear local cache, the cacheName is {}, the key is {}", cacheMessage.getCacheName(), cacheMessage.getKey());
     redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey());
     }
    }
    spring boot 설정 클래스 추가
    
    package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.AutoConfigureAfter;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
    import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.listener.ChannelTopic;
    import org.springframework.data.redis.listener.RedisMessageListenerContainer;
    import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.CacheMessageListener;
    import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.RedisCaffeineCacheManager;
    /** 
     * @author fuwei.deng
     * @date 2018 1 26    5:23:03
     * @version 1.0.0
     */
    @Configuration
    @AutoConfigureAfter(RedisAutoConfiguration.class)
    @EnableConfigurationProperties(CacheRedisCaffeineProperties.class)
    public class CacheRedisCaffeineAutoConfiguration {
     
     @Autowired
     private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;
     
     @Bean
     @ConditionalOnBean(RedisTemplate.class)
     public RedisCaffeineCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
     return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate);
     }
     
     @Bean
     public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate<Object, Object> redisTemplate, 
      RedisCaffeineCacheManager redisCaffeineCacheManager) {
     RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
     redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory());
     CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisCaffeineCacheManager);
     redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(cacheRedisCaffeineProperties.getRedis().getTopic()));
     return redisMessageListenerContainer;
     }
    }
    
    
    resources/META-INF/spring.factories 파일 에 spring boot 설정 스 캔 추가
    
    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineAutoConfiguration
    
    이제 Maven 도입 으로 사용 할 수 있 습 니 다.
    
    <dependency>
      <groupId>com.itopener</groupId>
      <artifactId>cache-redis-caffeine-spring-boot-starter</artifactId>
      <version>1.0.0-SNAPSHOT</version>
      <type>pom</type>
    </dependency>
    시작 클래스 에@EnableCashing 주 해 를 추가 하고 캐 시가 필요 한 방법 에@Cacheable 주 해 를 추가 합 니 다.
    
    package com.itopener.demo.cache.redis.caffeine.service;
    import java.util.Random;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.CachePut;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    import com.itopener.demo.cache.redis.caffeine.vo.UserVO;
    import com.itopener.utils.TimestampUtil;
    
    @Service
    public class CacheRedisCaffeineService {
     
     private final Logger logger = LoggerFactory.getLogger(CacheRedisCaffeineService.class);
    
     @Cacheable(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager")
     public UserVO get(long id) {
     logger.info("get by id from db");
     UserVO user = new UserVO();
     user.setId(id);
     user.setName("name" + id);
     user.setCreateTime(TimestampUtil.current());
     return user;
     }
     
     @Cacheable(key = "'cache_user_name_' + #name", value = "userNameCache", cacheManager = "cacheManager")
     public UserVO get(String name) {
     logger.info("get by name from db");
     UserVO user = new UserVO();
     user.setId(new Random().nextLong());
     user.setName(name);
     user.setCreateTime(TimestampUtil.current());
     return user;
     }
     
     @CachePut(key = "'cache_user_id_' + #userVO.id", value = "userIdCache", cacheManager = "cacheManager")
     public UserVO update(UserVO userVO) {
     logger.info("update to db");
     userVO.setCreateTime(TimestampUtil.current());
     return userVO;
     }
     
     @CacheEvict(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager")
     public void delete(long id) {
     logger.info("delete from db");
     }
    }
    
    properties 파일 에서 redis 설정 은 redis 를 사용 하 는 것 과 같 습 니 다.2 급 캐 시 설정 을 추가 할 수 있 습 니 다.
    
    #       
    spring.cache.multi.caffeine.expireAfterAccess=5000
    spring.cache.multi.redis.defaultExpiration=60000
    
    #spring cache  
    spring.cache.cache-names=userIdCache,userNameCache
    
    #redis  
    #spring.redis.timeout=10000
    #spring.redis.password=redispwd
    #redis pool
    #spring.redis.pool.maxIdle=10
    #spring.redis.pool.minIdle=2
    #spring.redis.pool.maxActive=10
    #spring.redis.pool.maxWait=3000
    #redis cluster
    spring.redis.cluster.nodes=127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005,127.0.0.1:7006
    spring.redis.cluster.maxRedirects=3
    넓히다
    개인 적 으로 redisson 의 포장 이 더 편리 하 다 고 생각 합 니 다.
  • spring cache 캐 시 실현 에 그렇게 많은 결함 이 없다
  • redis 의 HASH 구 조 를 사용 하여 서로 다른 hashkey 에 대해 만 료 시간 을 설정 할 수 있 고 청소 할 때 더욱 편리 합 니 다
  • redisson 을 기반 으로 다단 계 캐 시 를 실현 하면 RedissonCache 를 계승 할 수 있 고 해당 방법 에 1 급 캐 시 를 추가 하면 됩 니 다
  • 분포 식 잠 금 을 사용 하 는 경우 가 있 으 면 더욱 편리 하 며,Redisson 에 포 장 된 분포 식 잠 금
  • 을 직접 사용 할 수 있다.
  • redisson 의 게시 구독 패키지 가 더 잘 사용 되 었 습 니 다
  • 후속 적 으로 캐 시 명중률 에 대한 통계 endpoint 를 증가 시 킬 수 있 습 니 다.그러면 각 캐 시 명중 상황 을 잘 감시 하여 캐 시 설정 을 최적화 할 수 있 습 니 다.
    원본 코드 다운로드
    starter 디렉토리:springboot/itopener-parent/spring-boot-starters-parent/cache-redis-caffeine-spring-boot-starter-parent
    예제 코드 디 렉 터 리:springboot/itopener-parent/demo-parent/demo-cache-redis-caffeine
    이상 이 바로 본 고의 모든 내용 입 니 다.여러분 의 학습 에 도움 이 되 고 저 희 를 많이 응원 해 주 셨 으 면 좋 겠 습 니 다.

    좋은 웹페이지 즐겨찾기