本文主要讲解springboot项目如何进行缓存配置
- 分布式缓存
- 本地缓存
- 自定义缓存
其实更重要的是这三种缓存的使用场景,我先详细说下
- 分布式缓存 redis 基本上算是最常见的缓存模式了,一般项目都会接入 redis 缓存 ,在登陆授权,菜单鉴权,数据缓存如商品详情缓存等场景,一般都是分布式redis 解决不了的问题 才会考虑本地缓存 和自定义缓存
- 本地缓存caffieine ,使用场景 无疑是高并发 场景, redis 虽然解决了分布式数据一致性问题,但是 通过tcp网络通信,在高并发场景,响应速度不及本地缓存,bigKey 对 redis本身性能影响很大,同时占用了大量的网络带宽,还会影响其他服务的通信质量,很容易成为性能瓶颈,如商品详情,店铺自定义页,系统参数查询 这种高频访问,不经常修改 或占用很多内存大小的数据, 本地缓存无疑是最好的,简单一句就是 hotkey bigKey 都可以考虑使用本地缓存 对系统优化有奇效
- 接下来要说的是 自定义缓存, 自定义缓存其实就是为了解决分布式缓存和本地缓存 各自的缺点,同时又想使用各自的优点,怎么办那就自定义实现,实现本地缓存的一致性通信,解决本地缓存的一致性问题,以及分布式缓存的性能问题,同时还可以加入hotkey热点探测 能功能,选择性的将hotkey放在本地,以及 对缓存访问进行指标收集,告警通知,已经自定义接口实现,屏蔽掉一些 危险命令 如 keys等,能做的事情很多,所以自定义缓存无疑是最好的,更灵活高效,同时也更考验程序员的能力
分布式缓存-RedisCacheManager 配置
一般来说 平时的springboot项目都是接入接入redis 来作为CacheManager 实现 ,正常的配置文件如下: 配置 一个RedisTemplate 和 CacheManager ,回头你再搞个redisUtil 封装下RedisTemplate的方法就行了
package com.vyibc.boot.test.j2cache.service.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public CacheManager initRedisCacheManager(RedisConnectionFactory connectionFactory)
{
RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager
.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory);
return builder.build();
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//序列化设置 ,这样为了存储操作对象时正常显示的数据,也能正常存储和获取
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(factory);
return stringRedisTemplate;
}
}
这时候 CacheManager的实现类 是RedisCacheManager,因此你搞一个含有@Cacheable 注解的缓存类RedisCacheService,以及缓存测试方法 ReidisCacheServiceTest
package com.vyibc.boot.test.j2cache.service;
import com.vyibc.boot.test.j2cache.service.model.TestBean;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class RedisCacheService {
private final AtomicInteger num = new AtomicInteger(0);
@Cacheable(cacheNames="test" ,key = "'a_num'")
public Integer getNum() {
return num.incrementAndGet();
}
@Cacheable(cacheNames="testBean",key = "'a_bean'")
public TestBean testBean() {
TestBean bean = new TestBean();
bean.setNum(num.incrementAndGet());
return bean;
}
@CacheEvict(cacheNames={"test","testBean"},key = "'a_num'")
public void evict() {
System.out.println("num = " + num.get());
System.out.println("模拟的 db删除 " );
}
public void reset() {
num.set(0);
}
}
package com.vyibc.boot.test.j2cache.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;
import java.io.IOException;
/**
* <p>
*
* </p>
*
* @author huchangfeng
* @since 2023/7/3
*/
@SpringBootTest
class ReidisCacheServiceTest {
@Autowired
private J2CacheService j2CacheService;
@Test
public void testCache() throws IOException {
j2CacheService.reset();
j2CacheService.evict();
j2CacheService.getNum();
j2CacheService.getNum();
j2CacheService.getNum();
j2CacheService.getNum();
j2CacheService.getNum();
j2CacheService.getNum();
Integer n = j2CacheService.getNum();
Assert.isTrue(n == 1, "缓存未生效!");
System.out.println("缓存生效!");
}
}
执行下 testCache 可以看到 缓存生效了
当然 我自己的配置 肯定要优化成 LettuceConnectionFactory 的 具体配置如下
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.vyibc.boot.test.j2cache.service.config;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.Collections;
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
private static final Logger log = LoggerFactory.getLogger(RedisConfig.class);
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Bean("redisConnectionFactory")
public LettuceConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
config.setPassword(password);
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(config);
lettuceConnectionFactory.afterPropertiesSet(); // 初始化连接工厂
return lettuceConnectionFactory;
}
public RedisConfig() {
}
@Bean
public StringRedisSerializer stringRedisSerializer() {
return new StringRedisSerializer();
}
@Bean
public RedisTemplate<String, Object> redisTemplate(@Qualifier("redisConnectionFactory")LettuceConnectionFactory lettuceConnectionFactory) {
log.info(" --- redis config init --- ");
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = this.jacksonSerializer();
RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public CacheManager initRedisCacheManager(RedisConnectionFactory connectionFactory)
{
RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager
.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory);
return builder.build();
}
private Jackson2JsonRedisSerializer jacksonSerializer() {
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, Visibility.ANY);
objectMapper.enableDefaultTyping(DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
}
本地缓存-CaffeineCacheManager 配置
接下来要说的是 如果我想使用本地缓存作为cacheManager实现 如 CaffeineCacheManager,这个场景其实在高并发场景 很常见,高并发场景本地缓存是最有效的 解决方案,目前本地缓存综合性能最好的无疑是 Caffeine ,当然 怎么保证本地缓存一致性,热可以探测等 是另外必须要做的事情,后续文章会详细说明怎么实现。
如何自定义一个
SpringBoot系列教程之内存缓存Caffiene自定义CacheManager
<dependencies>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>
package com.vyibc.boot.test.j2cache.service;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean(name = "oneHourCacheManager")
public CacheManager oneHourCacheManager(){
Caffeine caffeine = Caffeine.newBuilder()
.initialCapacity(10) //初始大小
.maximumSize(11) //最大大小
.expireAfterWrite(1, TimeUnit.HOURS); //写入/更新之后1小时过期
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setAllowNullValues(true);
caffeineCacheManager.setCaffeine(caffeine);
return caffeineCacheManager;
}
}
此时 就完成了使用本地缓存作为cacheManager实现 这个也是比较简单的,毕竟CaffeineCacheManager 已经被写好了
自定义缓存-自定义CacheManager 实现
接下来是 如何完全自定义自己的缓存实现类 其实也很简单 我们可以参考 spring-context-support 是如何做的 仿着做一下就行了,基本上我们在查看中间价的时候
主要分两步
- 写一个cache类 实现 AbstractValueAdaptingCache接口
- 大概逻辑:里面注入你自己的缓存容器如CaffeineCache 的 cache属性 并实现 接口逻辑 就是缓存的增删改查 逻辑
- 写一个cacheManager类 实现接口CacheManager 或者 AbstractTransactionSupportingCacheManager
- 大概逻辑: 构造一个 缓存命名空间-->cache 的 map容器 实现接口逻辑, 注入你上面写的 cache
这样我们就可以获取缓存的逻辑就是, 缓存命名空间 获取的cache ,然后执行cache 的查找逻辑,查不到就去执行 真正的方法
多说无益 ,直接看CaffeineCache 怎么做的 照着写就好了,然后自定义自己的逻辑 如多级缓存的实现