spring boot:使用caffeine+redis做二级缓存(spring boot 2.3.1)
一,为什么要使用二级缓存?
我们通常会使用caffeine做本地缓存(或者叫做进程内缓存),
它的优点是速度快,操作方便,缺点是不方便管理,不方便扩展
而通常会使用redis作为分布式缓存,
它的优点是方便扩展,方便管理,但速度上肯定比本地缓存要慢一些,因为有网络io
所以在生产环境中,我们通常把两者都启用,
这样本地缓存做为一级缓存,虽然容量不够大,但也可以把热点数据缓存下来,
把高频访问拦截在redis的上游,
而redis做为二级缓存,把访问请求拦截在数据库的上游,
归根到底,这样可以更有效的减少到数据库的访问,
从而减轻数据库的压力,支持更高并发的访问
说明:刘宏缔的架构森林是一个专注架构的博客,
网站:https://blog.imgtouch.com
本文: https://blog.imgtouch.com/index.php/2023/05/24/springboot-shi-yong-caffeineredis-zuo-er-ji-huan-cun-springboot231/
对应的源码可以访问这里获取: 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)