springboot 双缓存设计与使用

前言

在日常开发中,缓存可以说已经成为必不可少的使用,在很多场景下,缓存带来的效果是非常明显的,可以有效缓解系统洪峰对数据库造成的压力,提升系统整体的接口响应,因此一个比较常用的数据存储设计结构就是:mysql + redis ,即数据库与缓存数据库的搭配

在springcache使用详解一篇中,我们详细剖析了springboot项目中使用redis作为缓存的用法,即通过相关的注解就可以完成很多常见缓存场景

业务痛点

在掌握了mysql + redis 这种常用的缓存设计结构的使用后,有必要作一番更深入的思考,这种缓存架构是不是最好呢?或者说有没有缺陷呢?应该说,在应对10万加的数据量左右的项目来说是绰绰有余了(本人所经手的其中一个项目数据量级),但事事不是绝对的,在生产环境下就出现了下面的一个生产故障

故障描述

A接口,dubbo接口,该接口承载的主要业务功能为:根据用户ID查询用户信息

由于是平台级的SASS应用,该dubbo接口被超过15个其他的上层应用调用,通过统计发现,该接口在本产品所有提供出去的dubbo接口中,调用的频率是最高的,一天下来,最大的调用次数为1.6万 + ,峰值时,平均每分钟调用次数超过2000+,如此庞大的调用量,对接口提供方所在产品的服务器压力非常大

为了环境这种密集查询带来的数据库开销,开始设计的时候,也是采用了springcache的缓存方案,核心的dubbo接口中服务层代码如下:

 	@Override
    @Cacheable
    public UserDTO getById(String userId) {
        Assert.userIdIsBlank(apikey);
        return userService.getById(userId);
    }

对springcache有所了解的同学应该对上面的代码不陌生,上面的代码放在生产环境下运行了一年多未见异常,但是近期的某几天,却出现了在一天中的3个时刻,其他应用调用该接口时报获取不到用户数据的异常

最终通过问题定位分析发现,获取不到数据是因为redis的连接超时;

为什么会超时呢?

这也就是上文所说,该接口的调用频次太高了,如果不加缓存,压力将会在mysql这一侧,但是加了缓存的话,那么在业务高峰的时候,压力将会首先转移给了redis,因为项目中的springcache使用的是redis作为存储介质,这样一来,请求过来之后,既然要查缓存,必然要建立redis连接了

我们知道,尽管redis4.0之后出现了多线程并支持异步,但是项目与redis需要建立的连接数是有限的啊,总不能无限多的请求过来就无限的建立新的连接吧,于是就出现了后来的请求终于在某个时刻无法通过建立新的连接快速响应时,抛出了连接超时异常

搞清楚了问题的原因,于是我们着手分析如何解决这个问题

redis + caffeine 双缓存

最终通过研讨会议,确定了采用redis + caffeine 双缓存的设计来改善目前因为上面的问题引发的缓存缺陷,最终的处理思路就是,针对当前这个接口,以及系统中其他类似的接口做了基本的分类,一部分接口仍然采用 springcache 的redis缓存存储方式,另一部分采用caffeine 作为缓存,这样确保了业务高峰期时,减少大量的缓存连接打到redis上

关于Caffeine Cache

  • Google Guava Cache是一种非常优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式。
  • 基于容量的方式内部实现采用LRU算法,基于引用回收很好的利用了Java虚拟机的垃圾回收机制。
  • 其中的缓存构造器CacheBuilder采用构建者模式提供了设置好各种参数的缓存对象,缓存核心类LocalCache里面的内部类Segment与jdk1.7及以前的ConcurrentHashMap非常相似,都继承于ReetrantLock,还有六个队列,以实现丰富的本地缓存方案。 ​

通俗的讲,Guva是google开源的一个公共java库,类似于Apache Commons,它提供了集合,反射,缓存,科学计算,xml,io等一些工具类库。cache只是其中的一个模块。使用Guva cache能够方便快速的构建本地缓存。

Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中将取代Guava。如果出现Caffeine,CaffeineCacheManager将会自动配置。

为什么要用本地缓存

相对于IO操作 ,速度快,效率更高 ,相对于Redis Redis是一种优秀的分布式缓存实现,受限于网卡等原因,远水救不了近火,属于内存级缓存

DB + Redis + LocalCache = 高效存储,高效访问

caffeine 使用场景

  • 愿意消耗一些内存空间来提升速度
  • 预料到某些键会被多次查询
  • 缓存中存放的数据总量不会超出内存容量

使用步骤

1、maven中添加caffeine 依赖

        
            com.github.ben-manes.caffeine
            caffeine
            3.0.1
        

2、在配置类中添加CaffeineCacheManager配置

里面要设置的参数可以说见名知意,就不过多解释了,可以与springcahe的RedisCacheConfiguration 配置在同一个类中

 	/**
     * 正常时间的本地缓存
     */
    @Bean("caffeineCacheManager")
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(30, TimeUnit.SECONDS)
                .initialCapacity(256)
                .maximumSize(10000));
        return cacheManager;
    }

3、在业务实现方法上使用上面的caffeineCacheManager

比如,我们根据用户ID查询用户信息,使用时,只需要显式的指定cacheManager 的类型即可生效,而其他的注解,比如前面的 @Cacheable ,这个和springcache通用注解是一致的

	@Override
    @Cacheable(value = {"dbUser"},key = "#root.args[0]",cacheManager = "caffeineCacheManager")
    public DbUser getByIdFromCaffeine(String id) {
        System.out.println("查询数据库");
        DbUser dbUser = dbUserMapper.getByUserId(id);
        return dbUser;
    }

4、编写一个接口,测试缓存是否生效

	@GetMapping("/getByIdFromCaffeine")
    public DbUser getByIdFromCaffeine(String id){
        return dbUserService.getByIdFromCaffeine(id);
    }

启动工程,浏览器调用上面的接口,
在这里插入图片描述
在这里插入图片描述
反复多刷几次,发现控制台不再打印 查询数据库 的这几个字,说明缓存生效了

具体来说,caffeine的配置很简单,主要集中在那个自定义的CaffeineCacheManager 中,可以根据自身的情况,对缓存过期时间,缓存数量的大小数量做合理的配置,下面给出关于CaffeineCacheManager 所在类的完整配置,在该类中,可以搭配redis的方式一起使用,方便学习和查阅

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(mapper);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    /**
     * 分钟级别
     * @param connectionFactory
     * @return
     */
    @Bean("cacheManagerMinutes")
    public RedisCacheManager cacheManagerMinutes(RedisConnectionFactory connectionFactory){
        RedisCacheConfiguration configuration = instanceConfig(3 * 60L);
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(configuration)
                .transactionAware()
                .build();
    }

    /**
     * 小时级别
     * @param connectionFactory
     * @return
     */
    @Bean("cacheManagerHour")
    @Primary
    public RedisCacheManager cacheManagerHour(RedisConnectionFactory connectionFactory){
        RedisCacheConfiguration configuration = instanceConfig(3600L);
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(configuration)
                .transactionAware()
                .build();
    }

    /**
     * 天级别
     * @param connectionFactory
     * @return
     */
    @Bean("cacheManagerDay")
    public RedisCacheManager cacheManagerDay(RedisConnectionFactory connectionFactory){
        RedisCacheConfiguration configuration = instanceConfig(3600 * 24L);;
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(configuration)
                .transactionAware()
                .build();
    }

    /**
     * 正常时间的本地缓存
     */
    @Bean("caffeineCacheManager")
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(30, TimeUnit.SECONDS)
                .initialCapacity(256)
                .maximumSize(10000));
        return cacheManager;
    }

    private RedisCacheConfiguration instanceConfig(long ttl){
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.configure(MapperFeature.USE_ANNOTATIONS,false);
        //只针对非空的值进行序列化
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        //将类型序列化到属性的json字符串
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(ttl))
                .disableCachingNullValues()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
    }

    /**
     * 自定义key生成策略
     * @return
     */
    @Bean("defaultSpringKeyGenerator")
    public KeyGenerator defaultSpringKeyGenerator(){
        return new KeyGenerator() {
            @Override
            public Object generate(Object o, Method method, Object... objects) {
                String key = o.getClass().getSimpleName() + "_"
                        + method.getName() +"_"
                        + StringUtils.arrayToDelimitedString(objects,"_");

                System.out.println("key :" + key);
                return key;
            }
        };
    }

}

开启springcache功能,只需要在工程的配置文件中做如下指定即可
springboot 双缓存设计与使用_第1张图片

最后,就可以在你需要使用缓存的类或者方法上面,添加那些springcache的通用注解就可以了

你可能感兴趣的:(技术总结,springboot双缓存,springboot双缓冲设计,springboot双缓存使用,springboot中的双缓存)