• spring boot:使用caffeine+redis做二级缓存(spring boot 2.3.1)


    一,为什么要使用二级缓存?

    我们通常会使用caffeine做本地缓存(或者叫做进程内缓存),

    它的优点是速度快,操作方便,缺点是不方便管理,不方便扩展

    而通常会使用redis作为分布式缓存,

    它的优点是方便扩展,方便管理,但速度上肯定比本地缓存要慢一些,因为有网络io

    所以在生产环境中,我们通常把两者都启用,

    这样本地缓存做为一级缓存,虽然容量不够大,但也可以把热点数据缓存下来,

    把高频访问拦截在redis的上游,

    而redis做为二级缓存,把访问请求拦截在数据库的上游,

    归根到底,这样可以更有效的减少到数据库的访问,

    从而减轻数据库的压力,支持更高并发的访问

    说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest

             对应的源码可以访问这里获取: https://github.com/liuhongdi/

    说明:作者:刘宏缔 邮箱: 371125307@qq.com

    二,演示项目的相关信息

    1,项目地址

    https://github.com/liuhongdi/twocache

    2,项目说明

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

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

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

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

    3,项目结构:如图

     

    三,配置文件说明

     1,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是否生效,

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

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

    所以要有相应的控制

    2,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代码说明

    1,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

    2,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的连接,

    注意对value的处理使用了Jackson2JsonRedisSerializer,否则不能直接保存一个对象

    3, 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这个注解来使本地缓存生效

    4,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

    五,测试效果

    1,访问地址:

    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毫秒

    2,具体的缓存时间可以根据自己业务数据的更新频率来确定 ,

         原则上:本地缓存的时长要比redis更短一些,

         因为redis中的数据我们通常会采用同步机制来更新,

         而本地缓存因为在各台web服务内部,
         所以时间上不要太长

    六,查看spring boot的版本:

      .   ____          _            __ _ _
     /\ / ___'_ __ _ _(_)_ __  __ _    
    ( ( )\___ | '_ | '_| | '_ / _` |    
     \/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v2.3.1.RELEASE)
  • 相关阅读:
    [AS3 3D Demo] Stage3D学习过程中开发的3个Demo
    NGUI学习笔记(一):官方视频学习记录
    关于继承MonoBehaviour的一些记录
    Jquery js框架使用
    Highcharts 图表js框架
    js上传控件 plupload 使用记录
    关于 web中 使用 java.net.URLEncoder.encode 要编码两次呢 , js的encodeURIComponent 同理
    跑测试没有web环境的情况
    sitemesh 学习之 meta 引入
    sitemesh 2.4 装饰器学习
  • 原文地址:https://www.cnblogs.com/architectforest/p/13357072.html
Copyright © 2020-2023  润新知