caffeine + redis自定义二级缓存

最近在项目中发现公司通过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 getCacheNames() {
        String cacheName = cacheManagerContainer.getCustomCacheProperties().getCacheName();
        List 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 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 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 jsonSerializer = new Jackson2JsonRedisSerializer(Object.class);
        RedisSerializationContext.SerializationPair jsonSerializationPair = RedisSerializationContext.SerializationPair
                .fromSerializer(RedisSerializer.java());
        RedisSerializationContext.SerializationPair 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> map = creatCaffeineCache(customCacheProperties);
        CustomCaffeineCacheManager customCaffeineCacheManager = new CustomCaffeineCacheManager(map);

        return customCaffeineCacheManager;
    }

    private Map> creatCaffeineCache(CustomCacheProperties customCacheProperties) {
        Map> map = new HashMap<>();

        Map 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 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> 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> map,boolean cacheNullValues) {
        log.info(">>>> CustomCaffeineCacheManager constructor start <<<<");
        this.map = map;
        this.cacheNullValues = cacheNullValues;
        super.setCacheNames(map.keySet());
    }

    protected Cache 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 get(Object key, Class 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 get(Object key, Callable 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自定义二级缓存)