zoukankan      html  css  js  c++  java
  • spring boot: 用redis的消息订阅功能更新应用内的caffeine本地缓存(spring boot 2.3.2)

    一,为什么要更新caffeine缓存?

    1,caffeine缓存的优点和缺点

    生产环境中,caffeine缓存是我们在应用中使用的本地缓存,

    它的优势在于存在于应用内,访问速度最快,通常都不到1ms就能做出响应,

    缺点在于不方便管理,因为存在于多台负载均衡的web服务器上,

    很难象管理redis缓存一样对它做出更新、删除。

    2,通常我们会把caffeine缓存的时间设置为5分钟或10分钟,

       但当有大型促销活动开始时,如果缓存还没过期,

       则web服务显示的数据不会立刻得到更新,

       我们如何更新多台web服务器的的应用内缓存?

       使用redis的消息订阅是解决方法之一,

       我们从后台发送一条消息到redis,

       订阅了redis的web服务收到消息可以对缓存进行处理,

       这样实现对多台web服务器上的缓存的更新

    3, 生产环境中通常会使用多级缓存,

        我们在更新caffeine缓存时,

        也不要去访问数据库,避免导致对数据库的并发访问,

        而是更新完redis后,

        本地缓存从redis获取数据,

        而几百几千数量级的并发访问对于redis来说压力很小

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

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

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

    二,演示项目的相关信息

    1,项目地址

    https://github.com/liuhongdi/redispubsub

    2,项目功能说明:

       web服务通过订阅redis的消息,

       实现对缓存的更新/删除/清除

    3,项目结构:如图:

    三,配置文件说明

    1,pom.xml

            <!--redis begin-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-pool2</artifactId>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>2.11.1</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>2.11.1</version>
            </dependency>
            <!--redis   end-->
    
            <!-- fastjson begin-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.73</version>
            </dependency>
            <!-- fastjson   end-->
    
            <!--local cache begin-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-cache</artifactId>
            </dependency>
            <dependency>
                <groupId>com.github.ben-manes.caffeine</groupId>
                <artifactId>caffeine</artifactId>
                <version>2.8.5</version>
            </dependency>
            <!--local cache   end-->
    
            <!--mybatis begin-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.3</version>
            </dependency>
            <!--mybatis end-->
    
            <!--mysql begin-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            <!--mysql end-->

    2,application.properties

    #error
    server.error.include-stacktrace=always
    #errorlog
    logging.level.org.springframework.web=trace
    
    #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
    
    #mysql
    spring.datasource.url=jdbc:mysql://localhost:3306/store?characterEncoding=utf8&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=lhddemo
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    
    #mybatis
    mybatis.mapper-locations=classpath:/mapper/*Mapper.xml
    mybatis.type-aliases-package=com.example.demo.mapper
    mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    
    #profile
    spring.profiles.active=cacheenable

    3,goods数据表的建表sql:

    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),       //有效期600秒, 最大容量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;
        }
    }

    说明:创建了两个缓存 goods,homePage

    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 redis1Template(
                @Qualifier("redis1LettuceConnectionFactory") LettuceConnectionFactory redis1LettuceConnectionFactory) {
            StringRedisTemplate redisTemplate = new StringRedisTemplate();
    
            //使用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的访问连接配置

    3,RedisListenerConfig.java

    @Configuration
    public class RedisListenerConfig {
    
        //创建两个消息监听器MessageListener
        @Bean
        RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                                MessageListenerAdapter listenerAdapter) {
            RedisMessageListenerContainer container = new RedisMessageListenerContainer();
            container.setConnectionFactory(connectionFactory);
            container.addMessageListener(listenerAdapter, new PatternTopic(Constants.CHANNEL_GOODS));
            container.addMessageListener(listenerAdapter, new PatternTopic(Constants.CHANNEL_HOME));
            return container;
        }
    
        //指定接收消息的类名和方法名
        @Bean
        MessageListenerAdapter listenerAdapter(RedisMessageReceiver messageReceiver) {
            System.out.println("listenerAdapter");
            return new MessageListenerAdapter(messageReceiver, "onReceiveMessage");
        }
    
        //指定StringRedisTemplate的生成
        @Bean
        StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
            return new StringRedisTemplate(connectionFactory);
        }
    }

    创建RedisMessageListenerContainer,创建两个消息队列的监听

    4,RedisMessageReceiver.java

    @Component
    public class RedisMessageReceiver {
        @Resource
        private LocalCacheService localCacheService;
        //收到消息后进行处理
        public void onReceiveMessage(String message,String channel) {
            message=message.replace("\"",""");
            message=message.replace(""{","{");
            message=message.replace("}"","}");
    
            Msg msg = JSON.parseObject(message, Msg.class);
            System.out.println(channel+":消息:"+msg.getMsgType()+";content:"+msg.getContent());
            if (channel.equals(Constants.CHANNEL_GOODS)) {
                if (msg.getMsgType().equals("deleteall")) {
                    localCacheService.deleteGoodsCacheAll();
                } else if (msg.getMsgType().equals("delete") || msg.getMsgType().equals("update")) {
                    String goodslist = msg.getContent();
                    String[] strArr = goodslist.split(",");
                    System.out.println(strArr);
    
                    for (int i = 0; i < strArr.length; ++i){
                        Long goodsId = Long.parseLong(strArr[i]);
                        if (msg.getMsgType().equals("update")) {
                            localCacheService.updateGoodsCache(goodsId);
                        } else if (msg.getMsgType().equals("delete")) {
                            localCacheService.deleteGoodsCache(goodsId);
                        }
                    }
                }
            }
        }
    }

    说明:收到消息后,根据消息内容进行处理,

            我们收到的针对商品缓存的消息有三类:deleteall,update,delete

            分别调用三个不同的处理方法

    5,LocalCacheServiceImpl.java

    @Service
    public class LocalCacheServiceImpl implements LocalCacheService {
    
        @Resource
        private RedisTemplate redis1Template;
    
        //更新缓存
        @CachePut(value = "goods", key="#goodsId")
        @Override
        public Goods updateGoodsCache(Long goodsId){
            System.out.println("get data from redis");
            Goods goodsr = (Goods) redis1Template.opsForValue().get("goods_"+String.valueOf(goodsId));
            return goodsr;
        }
    
        //删除缓存
        @CacheEvict(value = "goods" ,key = "#goodsId")
        @Override
        public void deleteGoodsCache(Long goodsId) {
            System.out.println("删除缓存 ");
        }
    
        //清除缓存
        @CacheEvict(value = "goods", allEntries=true)
        @Override
        public void deleteGoodsCacheAll() {
            System.out.println("已删除全部缓存 ");
        }
    }

    说明:实现了对缓存的处理

    6,HomeController.java

    @RestController
    @RequestMapping("/home")
    public class HomeController {
        @Resource
        private RedisTemplate redis1Template;
        @Resource
        private GoodsService goodsService;
        @Resource
        private CacheManager getCacheManager;
    
        //发清空缓存的消息
        @GetMapping("/deleteall")
        public String deleteall(){
            String ret = "清除缓存的消息已发出";
            //删除id为4的商品的缓存
            Msg msg_del = new Msg();
            msg_del.setMsgType("deleteall");
            msg_del.setContent("");
            redis1Template.convertAndSend("goodsCache",JSON.toJSONString(msg_del));
            return ret;
        }
    
        //发更新缓存和删除缓存的消息
        @GetMapping("/update")
        public String update(){
               String ret = "";
               int goodsId = 3;
                //更新redis
               System.out.println("get data from redis");
               String key = "goods_"+String.valueOf(goodsId);
               Goods goodsr = (Goods)redis1Template.opsForValue().get(key);
               ret = "更新前:<br/>"+goodsr.toString()+"<br/>";
               String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.sss").format(System.currentTimeMillis());
               goodsr.setGoodsName("更新后的商品名,更新时间:"+now);
               redis1Template.opsForValue().set(key,goodsr);
               Goods goodsr2 = (Goods)redis1Template.opsForValue().get(key);
               ret += "更新后:<br/>"+goodsr2.toString()+"<br/>";
    
                //发布消息,接收者更新本地cache
                Msg msg_up = new Msg();
                msg_up.setMsgType("update");
                msg_up.setContent("3,5");
            redis1Template.convertAndSend("goodsCache",JSON.toJSONString(msg_up));
    
            //删除id为4的商品的缓存
                Msg msg_del = new Msg();
                msg_del.setMsgType("delete");
                msg_del.setContent("4");
            redis1Template.convertAndSend("goodsCache",JSON.toJSONString(msg_del));
            return ret;
        }
    
    
        //商品详情 参数:商品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;
        }
    
        //统计,如果是生产环境,需要加密才允许访问
        @GetMapping("/stats")
        @ResponseBody
        public Object stats() {
            CaffeineCache caffeine = (CaffeineCache)getCacheManager.getCache("goods");
            Cache goods = caffeine.getNativeCache();
            String statsInfo="cache名字:goods<br/>";
            Long size = goods.estimatedSize();
            statsInfo += "size:"+size+"<br/>";
            ConcurrentMap map= goods.asMap();
            statsInfo += "map keys:<br/>";
            for(Object key : map.keySet()) {
                statsInfo += "key:"+key.toString()+";value:"+map.get(key)+"<br/>";
            }
            statsInfo += "统计信息:"+goods.stats().toString();
            return statsInfo;
        }
    }

    说明:更新/删除/清空缓存的操作 我们都是通过发送redis消息实现,

             在生产环境中,这些功能需要放到管理后台

    五,测试效果

    1,生成缓存:

    分别访问:

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

    使商品id分别为 3/4/5的这三件商品生成caffeine缓存

    查看效果:访问:

    http://127.0.0.1:8080/home/stats

    可以看到缓存的数据:

    cache名字:goods
    size:3
    map keys:
    key:3;value: Goods:goodsId=3 goodsName=100分电动牙刷 subject=好用到让你爱上刷牙 price=59.00 stock=15
    key:4;value: Goods:goodsId=4 goodsName=蜂蜜牛奶手工皂 subject=深入滋养,肌肤细腻嫩滑 price=70.00 stock=33
    key:5;value: Goods:goodsId=5 goodsName=紫光筷子筒 subject=紫光智护,干爽防潮更健康 price=189.00 stock=20
    统计信息:CacheStats{hitCount=3, missCount=6, loadSuccessCount=6, loadFailureCount=0, totalLoadTime=624491686, evictionCount=3, evictionWeight=3}

    2,更新缓存:访问:

    http://127.0.0.1:8080/home/update

    我们在这个update方法中实现了两项功能:

    更新了缓存中商品id为3的商品的名字

    删除了缓存中商品id为4的对象

    查看效果:,访问:

    http://127.0.0.1:8080/home/stats

    返回:

    cache名字:goods
    size:2
    map keys:
    key:3;value: Goods:goodsId=3 goodsName=更新后的商品名,更新时间:2020-08-06 15:21:49.049 subject=好用到让你爱上刷牙 price=59.00 stock=15
    key:5;value: Goods:goodsId=5 goodsName=紫光筷子筒 subject=紫光智护,干爽防潮更健康 price=189.00 stock=20
    统计信息:CacheStats{hitCount=1, missCount=3, loadSuccessCount=3, loadFailureCount=0, totalLoadTime=169516569, evictionCount=0, evictionWeight=0}

    可以看到缓存中商品id为3的对象商品名被更新,

    商品id为4的对象已被删除

    3,清除缓存:

    访问:

    http://127.0.0.1:8080/home/deleteall

    查看效果:访问:

    http://127.0.0.1:8080/home/stats

    返回:

    cache名字:goods
    size:0
    map keys:
    统计信息:CacheStats{hitCount=1, missCount=3, loadSuccessCount=3, loadFailureCount=0, totalLoadTime=169516569, evictionCount=0, evictionWeight=0}

    可以看到缓存名为goods的缓存中的对象已被清空

    六,查看spring boot版本

      .   ____          _            __ _ _
     /\ / ___'_ __ _ _(_)_ __  __ _    
    ( ( )\___ | '_ | '_| | '_ / _` |    
     \/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v2.3.2.RELEASE)
  • 相关阅读:
    根据不同的状态查询错误
    myeclipse修改了安装目录名字打不开解决方法
    前端c标签foreach传值给后台
    Could not find result map java.lang.Integer] with root cause
    the import XXXX cannot be resolved 解决方法
    当我的url请求会变成jsp页面路径时的解决办法
    当项目启动很久一直超时怎么办?
    前端里面的变量名字多一个“;”会有这么多的区别
    java String.format()的问题
    String.format
  • 原文地址:https://www.cnblogs.com/architectforest/p/13446725.html
Copyright © 2011-2022 走看看