美文网首页
caffeine + redis自定义二级缓存

caffeine + redis自定义二级缓存

作者: 非典型_程序员 | 来源:发表于2019-08-04 18:41 被阅读0次

    最近在项目中发现公司通过caffeine和redis实现二级缓存,redis大家都比较熟悉了,caffeine有点新鲜。所以自己找了下资料发现spring cache支持的缓存类型就有caffeine,关于caffeine我就不再具体的介绍了,感兴趣的小伙伴可以到网上搜索一下具体资料。spring cache关于caffeine的可以参考文档。使用起来还是比较简单的,下面通过caffeine和redis来自己实现一个二级缓存。另外网上也有一些实现的相关文章,比如下面这两篇:
    Spring Boot缓存实战 Redis + Caffeine 实现多级缓存
    Sprboot + spring cache implements two-level caching (redis + caffeine)
    不过在实现方式上会有略微的差别,下面开始自己实现。

    一、自定义Cache

    我们这里主要是自定义二级缓存,所以我们需要定义一个自己的缓存类,这个缓存类包括了两个成员变量,即一级缓存和二级缓存,简单点理解就是自定义的缓存是将caffeine和redis的缓存又进行了封装,代码如下:

    public class CaffeineRedisCache implements Cache {
        private String cacheName;
        // 一级缓存
        private Cache firstLevel;
        // 二级缓存
        private Cache secondLevel;
    
        public CaffeineRedisCache(String cacheName, Cache first, Cache second) {
            log.info(">>>> CaffeineRedisCache constructor start,params:cacheName={},firstCache={},secondCache={} <<<<",cacheName,first,second);
            this.cacheName = cacheName;
            this.firstLevel = first;
            this.secondLevel = second;
        }
        // 省略相关方法
        ......
    }
    

    这里是去实现Cache接口或者继承AbstractValueAdaptingCache都是可以的。重写相关的方法即可。
    这里只是定义了缓存类,那么这个缓存类的创建需要CacheManager来完成,所以我们还需要新建一个自定的CacheManager

    二、自定义CacheManger

    自定义CacheManager也是一样的,不管是实现接口还是继承相关的类,主要目的就是创建Cache,我这里直接实现CacheManager接口。这里我自己添加了一个类型为CacheManagerContainer(自定义辅助类,代码见下面的辅助类代码)的成员变量,它的作用其实比较简单,就为了维护需要用到的其他CacheManger,比如RedisCacheManagerCaffeineCacheManager等,以及自定义配置类,因为是自定义缓存,所以原来spring cache的配置不能再使用(自定义配置类见下),CacheManager代码如下:

    public class CaffeineRedisCacheManager implements CacheManager {
    
        private CacheManagerContainer cacheManagerContainer;
    
        public CaffeineRedisCacheManager(CacheManagerContainer cacheManagerContainer) {
            this.cacheManagerContainer = cacheManagerContainer;
        }
    
        @Override
        public Cache getCache(String name) {
            CacheManagerContainer.CacheManagers containers = cacheManagerContainer.getManagers(name);
    
            return new CaffeineRedisCache(name,containers.getLevelOne().getCache(name),containers.getLevelTwo().getCache(name));
        }
    
        @Override
        public Collection<String> getCacheNames() {
            String cacheName = cacheManagerContainer.getCustomCacheProperties().getCacheName();
            List<String> cacheNames = new ArrayList<>();
            cacheNames.add(cacheName);
            return cacheNames;
        }
    }
    

    上面代码只是粗略的实现,有些地方还有待优化之处。通过getCache方法,就会返回自定义的CaffeineRedisCache。而CacheManagerContainer通过其维护的相关CacheManager获取对应的Cache

    三、自定义配置类

    自定义配置类是因为我们需要指定缓存的类型、过期时间等等。关于缓存的类型根据自己的需要定义(见下辅助类代码的CacheType)。
    自定义配置目前只有5种可配置项。其中主要说下配置中的Map,这一点主要是为了更加灵活的使用自定义缓存,比如某种类型的数据只缓存redis,另外一种类型只缓存caffeine等等。所以添加了一个内部类Container,简单理解Container控制缓存的粒度更细。代码如下:

    @Configuration
    @ConfigurationProperties(prefix = "com.ypc.custom.cache")
    public class CustomCacheProperties {
        // 缓存名称
        private String cacheName;
        // 默认缓存类型
        private CacheType defaultCacheType;
    
        // 缓存多少秒过期,默认为0,不过期
        private Integer expireSecond;
    
        private Map<String,CustomCacheProperties.Container> containers;
    
        // 是否缓存null值
        private boolean cacheNullValues;
    
        public CustomCacheProperties() {
            this.defaultCacheType = CacheType.CAFFEINE_REDIS;
            this.expireSecond = 0;
            this.containers = new HashMap<>();
            this.cacheNullValues = false;
        }
        // 省略get set方法
        ......
    
        // 内部类,主要为了细化具体的缓存
        public static class Container {
            // 缓存类型
            private CacheType cacheType;
            // 最大大小
            private Integer maximumSize;
            // 初始容量
            private Integer initialCapacity;
            // 过期时间
            private Integer expireAfterAccess;
    
            public Container() {
            }
          //省略get set方法
          ......
    }
    

    四、辅助类代码

    辅助类主要是自定义的缓存类型,这里我定义了4种,分别表示不使用缓存、使用redis、使用caffeine、同时使用redis和caffeine。

    public enum CacheType {
    
        NONE,
        REDIS,
        CAFFEINE,
        CAFFEINE_REDIS;
    
        private CacheType() {
        }
    }
    

    另一个辅助类也是比较重要的,这个缓存类主要是在使用根据缓存类型获取到具体的CacheManager,这里需要理解一下。也就是说自定义使用何种缓存就应该返回对应的CacheManager。因为我们是自定义了二级缓存,每一及缓存都有对应的缓存处理器。比如缓存类型是REDIS,则缓存处理器应该是redisCacheManagernoneCacheManager。如果缓存类型是 CAFFEINE_REDIS,那么缓存处理器则切换成caffeineCacheManager(即自定义的CustomCaffeineCacheManager)和redisCacheManager

    public class CacheManagerContainer {
    
        private CacheManager redisCacheManager;
    
        private CacheManager caffeineCacheManager;
    
        private CacheManager noneCacheManager;
    
        private CustomCacheProperties customCacheProperties;
    
        public CustomCacheProperties getCustomCacheProperties() {
            return customCacheProperties;
        }
    
        private Map<String,CacheType> cacheTypeMap;
    
        public CacheManagerContainer() {
        }
    
        public CacheManagerContainer(CacheManager redisCacheManager, CacheManager caffeineCacheManager, CustomCacheProperties customCacheProperties) {
            this.redisCacheManager = redisCacheManager;
            this.caffeineCacheManager = caffeineCacheManager;
            this.noneCacheManager = new NoOpCacheManager();
            this.customCacheProperties = customCacheProperties;
            this.cacheTypeMap = new HashMap<>();
    
            for (Map.Entry entry : customCacheProperties.getContainers().entrySet()) {
                String key = (String) entry.getKey();
                CustomCacheProperties.Container container = (CustomCacheProperties.Container) entry.getValue();
                CacheType cacheType = container.getCacheType() == null ? customCacheProperties.getDefaultCacheType() : container.getCacheType();
                this.cacheTypeMap.put(key,cacheType);
            }
        }
    
        public CacheManagers getManagers(String name) {
            CacheManagers cacheManagers = null;
            CacheType cacheType = this.cacheTypeMap.get(name);
            if (CAFFEINE_REDIS.equals(cacheType)) {
                cacheManagers = new CacheManagerContainer.CacheManagers(caffeineCacheManager,redisCacheManager);
            } else if (CAFFEINE.equals(cacheType)) {
                cacheManagers = new CacheManagerContainer.CacheManagers(caffeineCacheManager,noneCacheManager);
            } else if (REDIS.equals(cacheType)){
                cacheManagers = new CacheManagerContainer.CacheManagers(redisCacheManager,noneCacheManager);
            } else {
                cacheManagers = new CacheManagerContainer.CacheManagers(noneCacheManager,noneCacheManager);
            }
            return cacheManagers;
        }
    
        public static class CacheManagers {
            private CacheManager levelOne;
            private CacheManager levelTwo;
    
            public CacheManagers(CacheManager firstLevel, CacheManager secondLevel) {
                this.levelOne = firstLevel;
                this.levelTwo = secondLevel;
            }
    
            public CacheManager getLevelOne() {
                return levelOne;
            }
    
            public CacheManager getLevelTwo() {
                return levelTwo;
            }
        }
    }
    

    这个类的内部类CacheManagers维护了所用到的CacheManager。通过CacheManagerContainer构造函数,将自定义配置的缓存类型注入到其成员变量cacheTypeMap,这样就可以根据不同的缓存类型最终返回不同的CacheManagers,从而返回到真正的CacheManager

    五、配置相关bean

    这里主要是注入自定义的相关bean,比如redisCacheManagerKeyGenerator等等。这些bean有些需要根据自定义配置类来设置,所以注入了自定义的配置类,比如缓存名称、缓存的时间等等。这里主要说明两点,一个是自定义的caffeineRedisCacheManager,这个是bean以来redisCacheManagercaffeineCacheManager,因为需要多个CacheManager,所以这里调用了一个自定义的辅助类CacheManagerContainer,这个类维护了多个CacheManager,这个会根据配置的缓存类型返回不同的CacheManager
    另一个是caffeineCacheManager,这个是我们自定义的一个CacheManager,之所以没有使用默认的CaffeineCacheManager是因为上面说过的,为了缓存的灵活性(即有多个container),可以理解为在自定义的caffeineCacheManager内部维护了多个Cache

    @Slf4j
    @Configuration
    @EnableConfigurationProperties({CustomCacheProperties.class})
    public class CaffeineRedisCacheConfig {
    
        @Autowired
        private CustomCacheProperties customCacheProperties;
    
        /**
         * 自定义RedisCacheManger
         * @param redisConnectionFactory
         * @return
         */
        @Bean("redisCacheManager")
        public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
            log.info(">>>> create redisCacheManager bean start <<<<");
            Integer expireSecond = customCacheProperties.getExpireSecond();
            RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
    
            // 指定key value的序列化类型
            RedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
            RedisSerializationContext.SerializationPair<Object> jsonSerializationPair = RedisSerializationContext.SerializationPair
                    .fromSerializer(RedisSerializer.java());
            RedisSerializationContext.SerializationPair<String> stringSerializationPair = RedisSerializationContext.SerializationPair
                    .fromSerializer(RedisSerializer.string());
    
            RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().serializeKeysWith(stringSerializationPair)
                    .serializeValuesWith(jsonSerializationPair);
    
            // 设置过期时间
            redisCacheConfiguration = redisCacheConfiguration.entryTtl(Duration.ofSeconds(expireSecond));
            // 是否缓存null值
            if (!customCacheProperties.isCacheNullValues()) {
                redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
            }
            RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,redisCacheConfiguration);
            return redisCacheManager;
        }
    
        /**
         * 自定义CaffeineCacheManager
         * @return
         */
        @Bean("caffeineCacheManager")
        public CacheManager caffeineCacheManager() {
            log.info(">>>> create caffeineCacheManager bean start <<<<");
            Map<String,Cache<Object,Object>> map = creatCaffeineCache(customCacheProperties);
            CustomCaffeineCacheManager customCaffeineCacheManager = new CustomCaffeineCacheManager(map);
    
            return customCaffeineCacheManager;
        }
    
        private Map<String,Cache<Object,Object>> creatCaffeineCache(CustomCacheProperties customCacheProperties) {
            Map<String,Cache<Object,Object>> map = new HashMap<>();
    
            Map<String,CustomCacheProperties.Container> containers = customCacheProperties.getContainers();
            for (Map.Entry entry : containers.entrySet()) {
                CustomCacheProperties.Container container = (CustomCacheProperties.Container) entry.getValue();
                Integer initialCapacity = container.getInitialCapacity();
                Integer maximumSize = container.getMaximumSize();
                Integer expireTime = container.getExpireAfterAccess();
    
                Caffeine<Object, Object> builder = Caffeine.newBuilder()
                        .initialCapacity(initialCapacity)
                        .maximumSize(maximumSize)
                        .expireAfterAccess(expireTime, TimeUnit.SECONDS)
                        .recordStats();
    
                map.put((String) entry.getKey(),builder.build());
            }
    
            return map;
        }
    
        /**
         * 自定义CaffeineRedisCacheManager
         * @param redisConnectionFactory
         * @return
         */
        @Primary
        @Bean("caffeineRedisCacheManager")
        public CacheManager caffeineRedisCacheManager(RedisConnectionFactory redisConnectionFactory) {
            log.info(">>>> create custom caffeineRedisCacheManager bean start <<<<");
            CacheManagerContainer cacheManagerContainer = new CacheManagerContainer(redisCacheManager(redisConnectionFactory),caffeineCacheManager(),customCacheProperties);
            CacheManager cacheManager = new CaffeineRedisCacheManager(cacheManagerContainer);
            return cacheManager;
        }
    
        /**
         * 自定义KeyGenerator
         * @return
         */
        @Bean
        public KeyGenerator keyGenerator() {
            return new KeyGenerator() {
                public Object generate(Object target, Method method, Object... params) {
                    StringBuilder stringBuilder = new StringBuilder();
                    stringBuilder.append(method.getName());
                    Object[] copy = params;
                    int length = params.length;
    
                    for(int i = 0; i < length; i++) {
                        Object object = copy[i];
                        stringBuilder.append(object.toString());
                    }
                    return stringBuilder.toString();
                }
            };
        }
    }
    

    redisCacheManager的序列化方式使用的是默认的JdkSerializationRedisSerializer,本来自己打算使用json进行序列化,但是在使用过程中出现了一点问题,这个问题一直也没解决,所以就先用默认序列化方式了。
    下面是自定义CaffeineCacheManager代码:

    public class CustomCaffeineCacheManager extends CaffeineCacheManager {
        private Map<String,Cache<Object,Object>> map = new HashMap();
        private boolean cacheNullValues;
        @Override
        protected org.springframework.cache.Cache createCaffeineCache(String name) {
            log.info(">>>> create CaffeineCache by name={} <<<<",name);
            return new CaffeineCache(name,createNativeCaffeineCache(name),cacheNullValues);
        }
    
        public CustomCaffeineCacheManager(Map<String, Cache<Object, Object>> map,boolean cacheNullValues) {
            log.info(">>>> CustomCaffeineCacheManager constructor start <<<<");
            this.map = map;
            this.cacheNullValues = cacheNullValues;
            super.setCacheNames(map.keySet());
        }
    
        protected Cache<Object,Object> createNativeCaffeineCache(String name) {
            return map.get(name);
        }
    }
    

    自定义的CustomCaffeineCacheManager继承了CaffeineCacheManager,并重写了createCaffeineCache方法。

    六、测试

    下面我将自定义的缓存类代码完整的贴一下,添加了很多日志,主要是为了显示具体缓存是走的Caffeine还是Redis,代码如下:

    public class CaffeineRedisCache implements Cache {
        private String cacheName;
        // 一级缓存
        private Cache firstLevel;
        // 二级缓存
        private Cache secondLevel;
        public CaffeineRedisCache(String cacheName, Cache first, Cache second) {
            log.info(">>>> CaffeineRedisCache constructor start,params:cacheName={},firstCache={},secondCache={} <<<<",cacheName,first,second);
            this.cacheName = cacheName;
            this.firstLevel = first;
            this.secondLevel = second;
        }
        @Override
        public String getName() {
            return cacheName;
        }
    
        @Override
        public Object getNativeCache() {
            log.info(">>>> getNativeCache method <<<<");
            return this;
        }
    
        @Override
        public ValueWrapper get(Object key) {
            log.info(">>>> get method key={} <<<<",key);
            ValueWrapper value = firstLevel.get(key);
            if (value == null) {
                // 二级缓存
                log.info(">>>> get cache object from second level <<<<");
                value = secondLevel.get(key);
                if (value != null) {
                    Object result = value.get();
                    firstLevel.put(key,result);
                }
            }
            return value;
        }
    
        @Override
        public <T> T get(Object key, Class<T> clazz) {
            log.info(">>>> get class method key={},clazz={} <<<<",key,clazz);
            T value = firstLevel.get(key, clazz);
            if (value == null) {
                log.info(">>>> get cache object from second level <<<<");
                value = secondLevel.get(key,clazz);
                if (value != null) {
                    firstLevel.put(key,value);
                }
            }
            return value;
        }
    
        @Override
        public <T> T get(Object key, Callable<T> callable) {
            log.info(">>>> get callable method key={},callable={} <<<<",key,callable);
            T result = null;
            result = firstLevel.get(key,callable);
            if (result != null)
                return result;
            else {
                return secondLevel.get(key,callable);
            }
        }
    
        @Override
        public void put(Object key, Object value) {
            log.info(">>>> put method,key={},value={} <<<<",key,value);
            firstLevel.put(key,value);
            secondLevel.put(key,value);
        }
    
        @Override
        public ValueWrapper putIfAbsent(Object key, Object value) {
            log.info(">>>> putIfAbsent method,key={},value={} <<<<",key,value);
            firstLevel.putIfAbsent(key,value);
            return secondLevel.putIfAbsent(key,value);
        }
    
        @Override
        public void evict(Object o) {
            secondLevel.evict(o);
            firstLevel.evict(o);
        }
    
        @Override
        public void clear() {
            secondLevel.clear();
            firstLevel.clear();
        }
    }
    

    另外application.properties的自定义配置如下:

    server.port=8090
    # 数据库配置
    spring.datasource.url=jdbc:postgresql://localhost:5432/pgsql?useSSL=false&characterEncoding=utf8
    spring.datasource.driver-class-name=org.postgresql.Driver
    spring.datasource.username=postgres
    spring.datasource.password=123456
            
    # 自定义缓存配置
    com.ypc.custom.cache.cacheName=cache_custom
    com.ypc.custom.cache.cacheNullValues=true
    com.ypc.custom.cache.expireSecond=10000
    com.ypc.custom.cache.defaultCacheType=REDIS
    
    # 自定义缓存容器配置
    com.ypc.custom.cache.containers.caffeine_cache.cacheType=CAFFEINE_REDIS
    com.ypc.custom.cache.containers.caffeine_cache.maximumSize=500
    com.ypc.custom.cache.containers.caffeine_cache.initialCapacity=100
    com.ypc.custom.cache.containers.caffeine_cache.expireAfterAccess=6000
    
    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
    spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
    
    spring.jpa.show-sql=true
    

    配置中默认使用的缓存是REDIS,但是caffeine_cache容器配置的类型是CAFFEINE_REDIS,也就是会使用caffeine作为一级缓存,redis做为二级缓存。这个容器名称需要和使用@Cacheable注解的缓存名称保持一致。
    到这里代码层面的东西已经差不多完成了,当然其实过程没有这么简单,这个过程中遇到了不少的问题,即使现在感觉还是比较混乱的,有机会的话代码还是要好好修改一下。下面通过一个简单的接口测试下自定义的缓存有没有生效。
    第一次访问时日志输出如下:

    2019-08-04 18:00:12.908  INFO 18468 --- [nio-8090-exec-3] c.y.s.c.cache.CaffeineRedisCache         : >>>> CaffeineRedisCache constructor start,params:cacheName=caffeine_cache,firstCache=org.springframework.cache.caffeine.CaffeineCache@54ea6172,secondCache=org.springframework.data.redis.cache.RedisCache@570f2df3 <<<<
    2019-08-04 18:00:14.548  INFO 18468 --- [nio-8090-exec-3] c.y.s.c.cache.CaffeineRedisCache         : >>>> get method key=caffeine <<<<
    2019-08-04 18:00:14.549  INFO 18468 --- [nio-8090-exec-3] c.y.s.c.cache.CaffeineRedisCache         : >>>> get cache object from second level <<<<
    2019-08-04 18:00:14.598  INFO 18468 --- [nio-8090-exec-3] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
    Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.sex as sex3_0_, user0_.userid as userid4_0_, user0_.username as username5_0_ from t_user user0_ where user0_.username=?
    2019-08-04 18:00:14.682  INFO 18468 --- [nio-8090-exec-3] c.y.s.c.cache.CaffeineRedisCache         : >>>> put method,key=caffeine,value=User{id=16, age=212, username='caffeine', sex='male', userid='54545'} <<<<
    

    日志第一行输出的是CaffeineRedisCache构造函数的执行,然后分别从一级缓存和二级缓存查找结果,因为缓存没有找到,所以这里输出了具体的sql,然后调用了put方法,将结果放入一级和二级缓存。
    接下来再次调用这个接口,日志输出如下:

    2019-08-04 18:05:10.223  INFO 18468 --- [nio-8090-exec-5] c.y.s.c.cache.CaffeineRedisCache         : >>>> CaffeineRedisCache constructor start,params:cacheName=caffeine_cache,firstCache=org.springframework.cache.caffeine.CaffeineCache@54ea6172,secondCache=org.springframework.data.redis.cache.RedisCache@570f2df3 <<<<
    2019-08-04 18:05:10.936  INFO 18468 --- [nio-8090-exec-5] c.y.s.c.cache.CaffeineRedisCache         : >>>> get method key=caffeine <<<<
    

    同样先执行CaffeineRedisCache构造函数,接着直接调用get方法,这时候是从一级缓存中获取到了结果,直接返回。通过工具看下redis里面的数据,图片如下:

    图-1.png
    因为使用的是jdk的序列化方式,所以看着没那么直观,说明自定义的缓存是成功的。这里有点不太满意的地方就是指定redis缓存的序列化方式为json一直是失败的,不管是Jackson2JsonRedisSerializer还是GenericJackson2JsonRedisSerializerRedisSerializer.json())网找找了一些资料,但是最终还是没有解决问题,所以最后改成了jdk的序列化策略。

    好了关于使用caffeine和redis实现二级缓存就先到这里,中间自己也是踩了不少的坑,最后算是勉强完成(redis缓存使用json序列化有时间再看下吧),不过这次学习对spring cache方面的了解又多了一层,感觉还是很有收获的。就实现来讲这部分并不算难,无非是将redis和caffeine的缓存结合起来使用而已,开头的两篇文章也是非常不错的。这次的代码我已经提交到github


    更正:经测试redis使用json序列化方式是可行的,但是使用Jackson2JsonRedisSerializer依然不可行,会报下列错误:

    java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to ***.**.***.***.
    

    使用GenericJackson2JsonRedisSerializerRedisSerializer.json())是可行的。

    相关文章

      网友评论

          本文标题:caffeine + redis自定义二级缓存

          本文链接:https://www.haomeiwen.com/subject/zncjdctx.html