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

2020 年 11 月 18 日 CSDN
作者 |  Garnett
来源 | Garnett的Java之路(ID:gh_009246af52d4)
头图 |  CSDN 下载自东方IC


前言


各位可以回顾下上篇 来自未来的缓存-Caffeine,带你揭开它的神秘面纱
在之前我们介绍了如何使用 Redis 或者 Caffeine 来做缓存,那么肯定会有人问,我用了 redis 已经很快了,为什么还要结合使用其他的缓存呢,缓存最大的作用确实是提高效率,但是随着业务需求的发展,业务体量的增大,多级缓存的作用就凸显了出来,接下来让我们盯紧了哦!

为什么要用多级缓存?


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

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

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

1、缓存的选择

  • 一级缓存:Caffeine是一个一个高性能的 Java 缓存库;使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。
  • 二级缓存:redis是一高性能、高可用的key-value数据库,支持多种数据类型,支持集群,和应用服务器分开部署易于横向扩展。

2、数据流向

数据读取流程

数据删除流程

解决思路

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

实战多级缓存的用法


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

1、项目说明

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

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

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

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

2、项目结构

3、配置文件说明

  • application.properties

#redis1spring.redis1.host=127.0.0.1spring.redis1.port=6379spring.redis1.password=lhddemospring.redis1.database=0
spring.redis1.lettuce.pool.max-active=32spring.redis1.lettuce.pool.max-wait=300spring.redis1.lettuce.pool.max-idle=16spring.redis1.lettuce.pool.min-idle=8
spring.redis1.enabled=1
#profilespring.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='商品表'

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

@Configurationpublic 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

5、测试效果

访问地址:
http://127.0.0.1:8080/home/goodsget?goodsid=3
查看控制台的输出:
get data from redisget data from mysqlcosttime aop 方法doafterreturning:毫秒数:395
因为caffeine/redis中都没有数据,可以看到程序从mysql中查询数据
costtime aop 方法doafterreturning:毫秒数:0
再次刷新时,没有从redis/mysql中读数据,直接从caffeine返回,使用的时间不足1毫秒
get data from rediscosttime aop 方法doafterreturning:毫秒数:8
本地缓存过期后,可以看到数据在从redis中获取,用时8毫秒
具体的缓存时间可以根据自己业务数据的更新频率来确定 ,原则上:本地缓存的时长要比redis更短一些,因为redis中的数据我们通常会采用同步机制来更新, 而本地缓存因为在各台web服务内部,所以时间上不要太长!

总结


本文介绍了多级缓存的原理以及用法,通过这些知识的介绍相信你也收获了不少。希望这篇文章可以带你了解多级缓存,知道在什么场景下可以使用!

福 利

CSDN旗下公众号全新搜索技能上线啦!

只要在公众号内回复消息

就能自动回复想搜索的内容啦!


现在体验有惊喜,每日参与搜索打卡,

连续打卡满3天、7天、14天

均有CSDN精美礼品相送 百分百有礼!快戳

每日体验CSDN公众号搜索功能打卡

    
    
      
更多精彩推荐
     
     
       
☞开源数据库再创里程碑,PingCAP 获 2.7 亿美元融资
         
         
           
☞全球数百万台 Mac 疑似因 Big Sur 更新险酿计算灾难,苹果官方回应来了!
JavaScript 稳居第一、C# 连续下跌,调查 17000 名程序员后有了这些新发现!
中国第一代程序员潘爱民的 30 年程序人生
     
     
       
漫画:什么是 “千年虫” 问题?
万万没想到 Java 中最重要的关键字竟然是这个!
  
  
    
点分享
点点赞
点在看
登录查看更多
1

相关内容

Redis 是一个使用 C 语言写成的,开源的 key-value 数据库。
【2020新书】使用Kubernetes开发高级平台,519页pdf
专知会员服务
66+阅读 · 2020年9月19日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
68+阅读 · 2020年1月17日
【书籍推荐】简洁的Python编程(Clean Python),附274页pdf
专知会员服务
173+阅读 · 2020年1月1日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
94+阅读 · 2019年12月4日
【干货】谷歌Joshua Gordon 《TensorFlow 2.0讲解》,63页PPT
专知会员服务
24+阅读 · 2019年11月2日
逆天啦!OpenCV4.1.2 CPU上人脸检测居然能跑到700+ FPS
自己动手撸一个分布式IM(即时通讯) 系统
51CTO博客
13+阅读 · 2019年3月20日
34个最优秀好用的Python开源框架
专知
9+阅读 · 2019年3月1日
重新体验NoSQL | 飞雪连天射白鹿 大数狂舞倚灵动(Lindorm)
阿里巴巴数据库技术
10+阅读 · 2018年12月25日
Tplmap - 扫描服务器端模板注入漏洞的开源工具
黑白之道
6+阅读 · 2018年9月11日
深度学习开发必备开源框架
九章算法
12+阅读 · 2018年5月30日
Golang高性能实战
架构文摘
4+阅读 · 2018年4月11日
开源巨献:阿里巴巴最热门29款开源项目
算法与数据结构
5+阅读 · 2017年7月14日
Stagnation Detection with Randomized Local Search
Arxiv
0+阅读 · 2021年1月28日
Arxiv
0+阅读 · 2021年1月28日
Arxiv
0+阅读 · 2021年1月26日
Arxiv
4+阅读 · 2018年1月15日
VIP会员
相关VIP内容
相关资讯
逆天啦!OpenCV4.1.2 CPU上人脸检测居然能跑到700+ FPS
自己动手撸一个分布式IM(即时通讯) 系统
51CTO博客
13+阅读 · 2019年3月20日
34个最优秀好用的Python开源框架
专知
9+阅读 · 2019年3月1日
重新体验NoSQL | 飞雪连天射白鹿 大数狂舞倚灵动(Lindorm)
阿里巴巴数据库技术
10+阅读 · 2018年12月25日
Tplmap - 扫描服务器端模板注入漏洞的开源工具
黑白之道
6+阅读 · 2018年9月11日
深度学习开发必备开源框架
九章算法
12+阅读 · 2018年5月30日
Golang高性能实战
架构文摘
4+阅读 · 2018年4月11日
开源巨献:阿里巴巴最热门29款开源项目
算法与数据结构
5+阅读 · 2017年7月14日
Top
微信扫码咨询专知VIP会员