暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Caffeine和Redis居然可以这么搭,想不到吧,爱了爱了

Garnett的Java之路 2020-12-08
873

上篇我们了解了进程缓存之王-Caffeine之后,各位是不是迫不及待想知道它怎么用啊,今天它来了!

前言

各位可以回顾下上篇来自未来的缓存-Caffeine,带你揭开它的神秘面纱


在之前我们介绍了如何使用Redis或者Caffeine来做缓存,那么肯定会有人问,我用了redis已经很快了,为什么还要结合使用其他的缓存呢,缓存最大的作用确实是提高效率,但是随着业务需求的发展,业务体量的增大,多级缓存的作用就凸显了出来,接下来让我们盯紧了哦!




一.  为什么要用多级缓存?

  • 如果只使用redis来做缓存我们会有大量的请求到redis,但是每次请求的数据都是一样的,假如这一部分数据就放在应用服务器本地,那么就省去了请求redis的网络开销,请求速度就会快很多。但是使用redis横向扩展很方便。


  • 如果只使用Caffeine来做本地缓存,我们的应用服务器的内存是有限,并且单独为了缓存去扩展应用服务器是非常不划算。所以,只使用本地缓存也是有很大局限性的。


至此我们是不是有一个想法了,两个一起用。将热点数据放本地缓存(一级缓存),将非热点数据放redis缓存(二级缓存)。

缓存的选择

  • 一级缓存:Caffeine是一个一个高性能的 Java 缓存库;使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。

  • 二级缓存:redis是一高性能、高可用的key-value数据库,支持多种数据类型,支持集群,和应用服务器分开部署易于横向扩展。

数据流向

数据读取流程

数据删除流程

解决思路

Spring 本来就提供了Cache的支持,最核心的就是实现Cache和CacheManager接口。


二.  实战多级缓存的用法

以下演示项目的代码在 公众号后台回复【多级缓存】可以自取哦!


项目说明

1.我们在项目中使用了两级缓存

2.本地缓存的时间为60秒,过期后则从redis中取数据,

3.如果redis中不存在,则从数据库获取数据,

4.从数据库得到数据后,要写入到redis

项目结构


配置文件说明

application.properties

    #redis1
    spring.redis1.host=127.0.0.1
    spring.redis1.port=6379
    spring.redis1.password=lhddemo
    spring.redis1.database=0


    spring.redis1.lettuce.pool.max-active=32
    spring.redis1.lettuce.pool.max-wait=300
    spring.redis1.lettuce.pool.max-idle=16
    spring.redis1.lettuce.pool.min-idle=8


    spring.redis1.enabled=1


    #profile
    spring.profiles.active=cacheenable

    说明:

    spring.redis1.enabled=1: 用来控制redis是否生效

    spring.profiles.active=cacheenable: 用来控制caffeine是否生效,

    在测试环境中我们有时需要关闭缓存来调试数据库,

    在生产环境中如果缓存出现问题也有关闭缓存的需求,

    所以要有相应的控制




    mysql中的表结构

      CREATE TABLE `goods` (
      `goodsId` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
      `goodsName` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'name',
      `subject` varchar(200) NOT NULL DEFAULT '' COMMENT '标题',
      `price` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '价格',
      `stock` int(11) NOT NULL DEFAULT '0' COMMENT 'stock',
      PRIMARY KEY (`goodsId`)
      ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品表'


      Java代码说明

      CacheConfig.java

        @Profile("cacheenable")   prod这个profile时缓存才生效
        @Configuration
        @EnableCaching 开启缓存
        public class CacheConfig {
        public static final int DEFAULT_MAXSIZE = 10000;
        public static final int DEFAULT_TTL = 600;


        private SimpleCacheManager cacheManager = new SimpleCacheManager();


        定义cache名称、超时时长(秒)、最大容量
        public enum CacheEnum{
        goods(60,1000), 有效期60秒 , 最大容量1000
        homePage(7200,1000), 有效期2个小时 , 最大容量1000
        ;
        CacheEnum(int ttl, int maxSize) {
        this.ttl = ttl;
        this.maxSize = maxSize;
        }
        private int maxSize=DEFAULT_MAXSIZE; 最大數量
        private int ttl=DEFAULT_TTL; 过期时间(秒)
        public int getMaxSize() {
        return maxSize;
        }
        public int getTtl() {
        return ttl;
        }
        }




        创建基于Caffeine的Cache Manager
        @Bean
        @Primary
        public CacheManager caffeineCacheManager() {
        ArrayList<CaffeineCache> caches = new ArrayList<CaffeineCache>();
        for(CacheEnum c : CacheEnum.values()){
        caches.add(new CaffeineCache(c.name(),
        Caffeine.newBuilder().recordStats()
        .expireAfterWrite(c.getTtl(), TimeUnit.SECONDS)
        .maximumSize(c.getMaxSize()).build())
        );
        }
        cacheManager.setCaches(caches);
        return cacheManager;
        }


        @Bean
        public CacheManager getCacheManager() {
        return cacheManager;
        }
        }

        作用:把定义的缓存添加到Caffeine




        RedisConfig.java


          @Configuration
          public class RedisConfig {


          @Bean
          @Primary
          public LettuceConnectionFactory redis1LettuceConnectionFactory(RedisStandaloneConfiguration redis1RedisConfig,
          GenericObjectPoolConfig redis1PoolConfig) {
          LettuceClientConfiguration clientConfig =
          LettucePoolingClientConfiguration.builder().commandTimeout(Duration.ofMillis(100))
          .poolConfig(redis1PoolConfig).build();
          return new LettuceConnectionFactory(redis1RedisConfig, clientConfig);
          }


          @Bean
          public RedisTemplate<String, String> redis1Template(
          @Qualifier("redis1LettuceConnectionFactory") LettuceConnectionFactory redis1LettuceConnectionFactory) {
          RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
          //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
          redisTemplate.setKeySerializer(new StringRedisSerializer());
          redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
          //使用StringRedisSerializer来序列化和反序列化redis的key值
          redisTemplate.setHashKeySerializer(new StringRedisSerializer());
          redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
          //开启事务
          redisTemplate.setEnableTransactionSupport(true);
          redisTemplate.setConnectionFactory(redis1LettuceConnectionFactory);
          redisTemplate.afterPropertiesSet();
          return redisTemplate;
          }


          @Configuration
          public static class Redis1Config {
          @Value("${spring.redis1.host}")
          private String host;
          @Value("${spring.redis1.port}")
          private Integer port;
          @Value("${spring.redis1.password}")
          private String password;
          @Value("${spring.redis1.database}")
          private Integer database;


          @Value("${spring.redis1.lettuce.pool.max-active}")
          private Integer maxActive;
          @Value("${spring.redis1.lettuce.pool.max-idle}")
          private Integer maxIdle;
          @Value("${spring.redis1.lettuce.pool.max-wait}")
          private Long maxWait;
          @Value("${spring.redis1.lettuce.pool.min-idle}")
          private Integer minIdle;


          @Bean
          public GenericObjectPoolConfig redis1PoolConfig() {
          GenericObjectPoolConfig config = new GenericObjectPoolConfig();
          config.setMaxTotal(maxActive);
          config.setMaxIdle(maxIdle);
          config.setMinIdle(minIdle);
          config.setMaxWaitMillis(maxWait);
          return config;
          }


          @Bean
          public RedisStandaloneConfiguration redis1RedisConfig() {
          RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
          config.setHostName(host);
          config.setPassword(RedisPassword.of(password));
          config.setPort(port);
          config.setDatabase(database);
          return config;
          }
          }
          }


          作用:生成redis的连接




          HomeController.java

            //商品详情 参数:商品id
            @Cacheable(value = "goods", key="#goodsId",sync = true)
            @GetMapping("/goodsget")
            @ResponseBody
            public Goods goodsInfo(@RequestParam(value="goodsid",required = true,defaultValue = "0") Long goodsId) {
            Goods goods = goodsService.getOneGoodsById(goodsId);
            return goods;
            }

            注意使用Cacheable这个注解来使本地缓存生效




            GoodsServiceImpl.java

              @Override
              public Goods getOneGoodsById(Long goodsId) {
              Goods goodsOne;
              if (redis1enabled == 1) {
              System.out.println("get data from redis");
              Object goodsr = redis1Template.opsForValue().get("goods_"+String.valueOf(goodsId));
              if (goodsr == null) {
              System.out.println("get data from mysql");
              goodsOne = goodsMapper.selectOneGoods(goodsId);
              if (goodsOne == null) {
              redis1Template.opsForValue().set("goods_"+String.valueOf(goodsId),"-1",600, TimeUnit.SECONDS);
              } else {
              redis1Template.opsForValue().set("goods_"+String.valueOf(goodsId),goodsOne,600, TimeUnit.SECONDS);
              }
              } else {
              if (goodsr.equals("-1")) {
              goodsOne = null;
              } else {
              goodsOne = (Goods)goodsr;
              }
              }
              } else {
              goodsOne = goodsMapper.selectOneGoods(goodsId);
              }
              return goodsOne;
              }

              作用:先从redis中得到数据,如果找不到则从数据库中访问,

              注意做了redis1enabled是否==1的判断,即:redis全局生效时,

              才使用redis,否则直接访问mysql

              测试效果

              访问地址:

                http://127.0.0.1:8080/home/goodsget?goodsid=3

                查看控制台的输出:

                  get data from redis
                  get data from mysql
                  costtime aop 方法doafterreturning:毫秒数:395

                  因为caffeine/redis中都没有数据,可以看到程序从mysql中查询数据

                    costtime aop 方法doafterreturning:毫秒数:0

                    再次刷新时,没有从redis/mysql中读数据,直接从caffeine返回,使用的时间不足1毫秒

                      get data from redis
                      costtime aop 方法doafterreturning:毫秒数:8

                      本地缓存过期后,可以看到数据在从redis中获取,用时8毫秒


                      具体的缓存时间可以根据自己业务数据的更新频率来确定 ,原则上:本地缓存的时长要比redis更短一些,因为redis中的数据我们通常会采用同步机制来更新, 而本地缓存因为在各台web服务内部,所以时间上不要太长!


                      总结

                      本文介绍了多级缓存的原理以及用法,通过这些知识的介绍相信你也收获了不少。希望这篇文章可以带你了解多级缓存,知道在什么场景下可以使用,Garnett还会不断的分享技术干货的,希望你们是我最好的观众!


                      乐于输出干货的Java技术公众号:Garnett的Java之路。公众号内有大量的技术文章、海量视频资源、精美脑图,不妨来关注一下!回复【资料】领取大量学习资源和免费书籍!


                      转发朋友圈是对我最大的支持!

                       觉得有点东西就点一下“赞和在看”吧!感谢大家的支持了!
                      文章转载自Garnett的Java之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                      评论