一,为什么要更新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)

spring boot: 用redis的消息订阅功能更新应用内的caffeine本地缓存(spring boot 2.3.2)的更多相关文章

  1. 【spring boot】【redis】spring boot 集成redis的发布订阅机制

    一.简单介绍 1.redis的发布订阅功能,很简单. 消息发布者和消息订阅者互相不认得,也不关心对方有谁. 消息发布者,将消息发送给频道(channel). 然后是由 频道(channel)将消息发送 ...

  2. 【springboot】【redis】springboot+redis实现发布订阅功能,实现redis的消息队列的功能

    springboot+redis实现发布订阅功能,实现redis的消息队列的功能 参考:https://www.cnblogs.com/cx987514451/p/9529611.html 思考一个问 ...

  3. 基于redis的消息订阅与发布

    Redis 的 SUBSCRIBE 命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端. 作为例子, 下图展示了频道 channel1  ...

  4. Redis的消息订阅及发布及事务机制

    Redis的消息订阅及发布及事务机制 订阅发布 SUBSCRIBE PUBLISH 订阅消息队列及发布消息. # 首先要打开redis-cli shell窗口 一个用于消息发布 一个用于消息订阅 # ...

  5. Spring Boot 2.x基础教程:使用Redis的发布订阅功能

    通过前面一篇集中式缓存的使用教程,我们已经了解了Redis的核心功能:作为K.V存储的高性能缓存. 接下来我们会分几篇来继续讲讲Redis的一些其他强大用法!如果你对此感兴趣,一定要关注收藏我哦! 发 ...

  6. Spring Boot使用Redis进行消息的发布订阅

    今天来学习如何利用Spring Data对Redis的支持来实现消息的发布订阅机制.发布订阅是一种典型的异步通信模型,可以让消息的发布者和订阅者充分解耦.在我们的例子中,我们将使用StringRedi ...

  7. spring boot 使用redis进行发布订阅

    异步消息的方式有很多,这篇博客介绍如何使用redis进行发布订阅, 完成这个示例只需要三个文件 1.redis消息监听配置 @Configuration public class RedisListe ...

  8. [SpingBoot guides系列翻译]Redis的消息订阅发布

    Redis的消息 部分参考链接 原文 CountDownLatch 概述 目的 这节讲的是用Redis来实现消息的发布和订阅,这里会使用Spring Data Redis来完成. 这里会用到两个东西, ...

  9. Java实现Redis的消息订阅和发布

    1.  首先需要一个消息监听器类 package com.sogou.baike.testimport.testSubscribe; import redis.clients.jedis.JedisP ...

随机推荐

  1. 预科班D2

    2020.09.08星期二 预科班D2 学习内容: 一.复习 1.平台: 平台=操作系统+计算机硬件 2.跨平台性 3.文件 文件是指操作系统提供给上层使用者操作硬盘的一种功能.

  2. vsCode 设置vue文件标签内的style智能提示

    VS Code 文件->首选项->设置 搜索:files.associations 点击在setting.json中编辑 最后一行添加配置: "files.association ...

  3. 深度神经网络conda环境下载

    介绍 因为使用conda下载数据有时候因为网络问题下载非常慢,因此我把conda的环境备份好,到时可以直接使用conda的conda create -n 新环境名字 –clone 环境的路径 , 直接 ...

  4. 阿里服务器docker部署

    首先本人是购买的阿里云服务器,虽然是1g的内存,不过部署一些项目还是没问题的,学生也有一个优惠服务器,好像是70多2g内存的,还是很舒服的,学生党可以试着部署一下,下面呢我就说一下我自己部署的步骤: ...

  5. k8s控制器资源

    k8s控制器资源   Pod pod在之前说过,pod是kubernetes集群中是最小的调度单元,pod中可以运行多个容器,而node又可以包含多个pod,关系如下图: 在对pod的用法进行说明之前 ...

  6. Docker多主机管理(八)

    docker多主机管理 前面我们的实验环境中只有一个 docker host,所有的容器都是运行在这一个 host 上的.但在真正的环境中会有多个 host,容器在这些 host 中启动.运行.停止和 ...

  7. hystrix文档翻译之Dashboard

    Dashboard Hystrix Dashboard可以让你实时监控hystrix的metrics信息. 当netflix开始使用dashboard后,运维效率得到了极大的提升,并且极大降低了大多数 ...

  8. 源码上看 .NET 中 StringBuilder 拼接字符串的实现

    前几天写了一篇StringBuilder与TextWriter二者之间区别的文章(链接).当时提了一句没有找到相关源码,于是随后有很多热心人士给出了相关的源码链接(链接),感谢大家.这几天抽了点时间查 ...

  9. php bypass disable function

    前言 最近开学,事太多了,好久没更新了,然后稍微闲一点一直在弄这个php bypass disable function,一开始自己的电脑win10安装蚁剑的插件,一直报错.怀疑是必须linux环境. ...

  10. 088 01 Android 零基础入门 02 Java面向对象 02 Java封装 01 封装的实现 02 封装的代码实现

    088 01 Android 零基础入门 02 Java面向对象 02 Java封装 01 封装的实现 02 封装的代码实现 本文知识点:Java封装的代码实现 说明:因为时间紧张,本人写博客过程中只 ...