前言
在之前我们介绍了如何使用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
#redis1spring.redis1.host=127.0.0.1spring.redis1.port=6379spring.redis1.password=lhddemospring.redis1.database=0spring.redis1.lettuce.pool.max-active=32spring.redis1.lettuce.pool.max-wait=300spring.redis1.lettuce.pool.max-idle=16spring.redis1.lettuce.pool.min-idle=8spring.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='商品表'
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秒 , 最大容量1000homePage(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@Primarypublic 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;}@Beanpublic CacheManager getCacheManager() {return cacheManager;}}
作用:把定义的缓存添加到Caffeine
RedisConfig.java
@Configurationpublic class RedisConfig {@Bean@Primarypublic LettuceConnectionFactory redis1LettuceConnectionFactory(RedisStandaloneConfiguration redis1RedisConfig,GenericObjectPoolConfig redis1PoolConfig) {LettuceClientConfiguration clientConfig =LettucePoolingClientConfiguration.builder().commandTimeout(Duration.ofMillis(100)).poolConfig(redis1PoolConfig).build();return new LettuceConnectionFactory(redis1RedisConfig, clientConfig);}@Beanpublic 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;}@Configurationpublic 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;@Beanpublic GenericObjectPoolConfig redis1PoolConfig() {GenericObjectPoolConfig config = new GenericObjectPoolConfig();config.setMaxTotal(maxActive);config.setMaxIdle(maxIdle);config.setMinIdle(minIdle);config.setMaxWaitMillis(maxWait);return config;}@Beanpublic 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")@ResponseBodypublic Goods goodsInfo(@RequestParam(value="goodsid",required = true,defaultValue = "0") Long goodsId) {Goods goods = goodsService.getOneGoodsById(goodsId);return goods;}
注意使用Cacheable这个注解来使本地缓存生效
GoodsServiceImpl.java
@Overridepublic 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 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服务内部,所以时间上不要太长!
总结
本文介绍了多级缓存的原理以及用法,通过这些知识的介绍相信你也收获了不少。希望这篇文章可以带你了解多级缓存,知道在什么场景下可以使用,Garnett还会不断的分享技术干货的,希望你们是我最好的观众!










