文章目录
Spring Cache
Spring针对不同的缓存技术,需要实现不同的cacheManager,
Spring定义了如下的cacheManger实现,具体使用哪种需要你自己实现。
pom
springboot 项目加入如下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 使用spring cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
配置redis的地址和端口号
redis:
host: 127.0.0.1
password :
port: 6379
timeout: 60s
配置缓存为redis
需要实现CacheManager用来具体实现缓存管理器
@Configuration
@EnableCaching
public class RedisCacheConfig {
private int defaultExpireTime=36000;//毫秒
private int userCacheExpireTime=10000;
private String userCacheName="cache";
/**
* 缓存管理器
*
* @param lettuceConnectionFactory
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory lettuceConnectionFactory) {
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
// 设置缓存管理器管理的缓存的默认过期时间
defaultCacheConfig = defaultCacheConfig.entryTtl(Duration.ofSeconds(defaultExpireTime))
// 设置 key为string序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// 设置value为json序列化
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
// 不缓存空值
.disableCachingNullValues();
Set<String> cacheNames = new HashSet<>();
cacheNames.add(userCacheName);
// 对每个缓存空间应用不同的配置
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put(userCacheName, defaultCacheConfig.entryTtl(Duration.ofSeconds(userCacheExpireTime)));
RedisCacheManager cacheManager = RedisCacheManager.builder(lettuceConnectionFactory)
.cacheDefaults(defaultCacheConfig)
.initialCacheNames(cacheNames)
.withInitialCacheConfigurations(configMap)
.build();
return cacheManager;
}
}
业务类启用注解
@Service
@CacheConfig(cacheNames="user")// cacheName 是一定要指定的属性,可以通过 @CacheConfig 声明该类的通用配置
public class UserServiceImpl {
/**
* 将结果缓存,当参数相同时,不会执行方法,从缓存中取
*
* @param id
* @return
*/
@Cacheable(key = "#id")
public User findUserById(Integer id) {
System.out.println("===> findUserById(id), id = " + id);
return new User(id, "taven");
}
/**
* 将结果缓存,并且该方法不管缓存是否存在,每次都会执行
*
* @param user
* @return
*/
@CachePut(key = "#user.id")
public User update(User user) {
System.out.println("===> update(user), user = " + user);
return user;
}
/**
* 移除缓存,根据指定key
*
* @param user
*/
@CacheEvict(key = "#user.id")
public void deleteById(User user) {
System.out.println("===> deleteById(), user = " + user);
}
/**
* 移除当前 cacheName下所有缓存
*
*/
@CacheEvict(allEntries = true)
public void deleteAll() {
System.out.println("===> deleteAll()");
}
}
运行即可看到,缓存数据到redis中
Spring Cache 注解
注解 | 作用 |
---|---|
@Cacheable | 将方法的结果缓存起来,下一次方法执行参数相同时,将不执行方法,返回缓存中的结果 |
@CacheEvict | 移除指定缓存 |
@CachePut | 标记该注解的方法总会执行,根据注解的配置将结果缓存 |
@Caching | 可以指定相同类型的多个缓存注解,例如根据不同的条件 |
@CacheConfig | 类级别注解,可以设置一些共通的配置,@CacheConfig(cacheNames=“user”), 代表该类下的方法均使用这个cacheNames |
Cacheable
@Cacheable使用两个或多个参数作为缓存的key
常见的如分页查询:使用单引号指定分割符,最终会拼接为一个字符串
@Cacheable(key = "#page+':'+#pageSize")
public List<User> findAllUsers(int page,int pageSize) {
int pageStart = (page-1)*pageSize;
return userMapper.findAllUsers(pageStart,pageSize);
}
findAllUsers会组成的key为1:10
当然还可以使用单引号自定义字符串作为缓存的key值
@Cacheable(key = "'countUsers'")
public int countUsers() {
return userMapper.countUsers();
}
CacheEvict
可以移除指定key
声明 allEntries=true移除该CacheName下所有缓存
声明beforeInvocation=true 在方法执行之前清除缓存,无论方法执行是否成功
//清除所有books下的实体
@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)
Caching
可以让你在一个方法上嵌套多个相同的Cache 注解(@Cacheable, @CachePut, @CacheEvict),分别指定不同的条件
//清除primary,secondary:deposit的缓存
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
默认 cache key
缓存的本质还是以 key-value 的形式存储的,默认情况下我们不指定key的时候 ,使用 SimpleKeyGenerator 作为key的生成策略
- 如果没有给出参数,则返回SimpleKey.EMPTY。
- 如果只给出一个Param,则返回该实例。
- 如果给出了更多的Param,则返回包含所有参数的SimpleKey。
注意:当使用默认策略时,我们的参数需要有 有效的hashCode()和equals()方法
实现原理
使用注解切入方法创建代理拦截器,实现调用方法之前之后执行关于redis相关的操作
@EnableCaching 注释触发后置处理器, 检查每一个Spring bean 的 public 方法是否存在缓存注解。如果找到这样的一个注释, 自动创建一个代理拦截方法调用和处理相应的缓存行为。
同步缓存-同步锁缓存
在多线程环境中,可能会出现相同的参数的请求并发调用方法的操作,默认情况下,spring cache 不会锁定任何东西,相同的值可能会被计算几次,这就违背了缓存的目的
对于这些特殊情况,可以使用sync属性。此时只有一个线程在处于计算,而其他线程则被阻塞,直到在缓存中更新条目为止。
@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {...}
条件缓存
- condition: 什么情况缓存,condition = true 时缓存,反之不缓存
- unless: 什么情况不缓存,unless = true 时不缓存,反之缓存
//name长度<32时缓存
@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)
//name长度<32时缓存 否则result不为空则缓存result为空则hardback
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)
高级点的东西
以上只是基本应用,我们可以自己定义序列化方式,和缓存key的前缀。多级缓存的实现,如果redis挂掉如何不影响业务正常运行?
CachingConfigurerSupport提供了如下几个方法,它可以让我们通过实现该方法实现更多选择
public class CachingConfigurerSupport implements CachingConfigurer {
public CachingConfigurerSupport() {
}
@Nullable
public CacheManager cacheManager() {//使用那种形式的缓存-redis还是Ecache
return null;
}
@Nullable
public CacheResolver cacheResolver() {//如何选择缓存器-多级缓存
return null;
}
@Nullable
public KeyGenerator keyGenerator() {//key的生成方案
return null;
}
@Nullable
public CacheErrorHandler errorHandler() {//异常处理
return null;
}
}
自定义StringSerializer和自定义缓存key前缀
实现自定义的序列化方式只需要实现redis序列化RedisSerializer接口即可,这里有两个关键方法,一个是序列化,一个是反序列化。同时在这个方法中也可以加入我们自定义的规则,比如统一把缓存放到一个全局的key前缀下面,这样就比较集中的方式实现缓存,避免散乱。
@Component
public class MyStringSerializer implements RedisSerializer<String> {
private final Logger logger = LoggerFactory.getLogger ( this.getClass () );
@Autowired
private RedisProperties redisProperties;
private final Charset charset;
public MyStringSerializer() {
this ( Charset.forName ( "UTF8" ) );
}
public MyStringSerializer(Charset charset) {
Assert.notNull ( charset, "Charset must not be null!" );
this.charset = charset;
}
@Override
public String deserialize(byte[] bytes) {
String keyPrefix = redisProperties.getKeyPrefix ();
String saveKey = new String ( bytes, charset );
int indexOf = saveKey.indexOf ( keyPrefix );
if (indexOf > 0) {
logger.info ( "key缺少前缀" );
} else {
saveKey = saveKey.substring ( indexOf );
}
logger.info ( "saveKey:{}",saveKey);
return (saveKey.getBytes () == null ? null : saveKey);
}
@Override
public byte[] serialize(String string) {
String keyPrefix = redisProperties.getKeyPrefix ();
String key = keyPrefix + string;
logger.info ( "key:{},getBytes:{}",key, key.getBytes ( charset ));
return (key == null ? null : key.getBytes ( charset ));
}
}
上面的序列化中加入了自定义key前缀
在上面的RedisCacheConfig里我们可以修改为自定义的序列化方式
@Autowired
MyStringSerializer myStringSerializer;
// 配置序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 7 天缓存过期
.entryTtl(Duration.ofSeconds(cacheConfigProperties.getTtlTime()))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(myStringSerializer))//自定义的key序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))//这里我们也可以修改成FastJson等第三方序列化
.disableCachingNullValues();
效果如下,这里我的前缀配置的是bamboo,其他和没有配置前缀没有差别,这样所有的注解缓存都会放入该子路径下面,可以做到一键清除所有缓存。
自定义fastJson请查看下文的参考内容
CacheResolver实现多级缓存
开发者可以通过自定义CacheResolver实现动态选择CacheManager,使用多种缓存机制:优先从堆内存读取缓存,堆内存缓存不存在时再从redis读取缓存,redis缓存不存在时最后从mysql读取数据,并将读取到的数据依次写到redis和堆内存中。
@Override
public CacheResolver cacheResolver() {
// 通过Guava实现的自定义堆内存缓存管理器
CacheManager guavaCacheManager = new GuavaCacheManager();
CacheManager redisCacheManager = redisCacheManager();
List<CacheManager> list = new ArrayList<>();
// 优先读取堆内存缓存
list.add(concurrentMapCacheManager);
// 堆内存缓存读取不到该key时再读取redis缓存
list.add(redisCacheManager);
return new CustomCacheResolver(list);
}
Redis故障或不可用时仍然执行方法服务可用
SimpleCacheErrorHandler直接抛出异常,我们可以重写org.springframework.cache.annotation.CachingConfigurerSupport.errorHandler方法自定义CacheErrorHandler操作缓存异常时异常处理。
重写errorHandler异常处理什么都不做即可,这里只是打印异常信息
@Override
public CacheErrorHandler errorHandler() {
CacheErrorHandler cacheErrorHandler = new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
RedisErrorException(exception, key);
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
RedisErrorException(exception, key);
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
RedisErrorException(exception, key);
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
RedisErrorException(exception, null);
}
};
return cacheErrorHandler;
}
protected void RedisErrorException(Exception exception,Object key){
logger.error("redis异常:key=[{}], exception={}", key, exception.getMessage());
}
直接关闭redis服务,然后访问接口,看控制台打印的记录如下,抛出异常后接着调用mysql查询出结果
2020-01-10 16:29:29,891 - redis key=bamboo:retailRatio::2,getBytes=[98, 97, 109, 98, 111, 111, 58, 114, 101, 116, 97, 105, 108, 82, 97, 116, 105, 111, 58, 58, 50]
2020-01-10 16:29:31,895 - redis异常:key=[2], exception=Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost:6379
2020-01-10 16:29:31,895 - ==> Preparing: select id, retail_role, unratio, ratio from j_retail_ratio where id = ?
2020-01-10 16:29:31,895 - ==> Parameters: 2(Integer)
2020-01-10 16:29:31,897 - <== Total: 1
推荐第三方maven
以上封装觉得麻烦可以直接使用中央仓库中我已经封装好的的版本
1.pom依赖
<dependency>
<groupId>com.github.bamboo-cn</groupId>
<artifactId>jt-common-core</artifactId>
<version>1.0.3</version>
</dependency>
2.Java启动类配置和业务层数据注解配置
//Java启动类配置
@SpringBootApplication(scanBasePackages = {"com.bamboo.common"})
//serviceImp需要使用注解
serviceImp类中的启用spring cache注解,用法同UserServiceImpl
3.yml配置自定义缓存配置-可以不用配置使用默认值
spring:
cache: #缓存配置
prefix: bamboo #key前缀
ttlTime: 2000 #缓存时长单位秒
cache配置默认值
- prefix: spring-cache
- ttlTime: 3600 一个小时
- 使用方式和spring cache注解相同
- 使用fastjson作为序列化
4.在1.0.3版本中已经支持单个key设置过期时长,从而避免使用默认过期时间
自定义单个key设置超时时间,key加上字符串TTL=1000实现扩展单个KEY过期时间
//@Cacheable(key = "T(String).valueOf(#code).concat('TTL=10')")
@Cacheable(key = "#code+'TTL=10'")
public Double getRetailByCode(Integer code) {
RetailRatio retailRatio = retailRatioMapper.selectByPrimaryKey(code);
return retailRatio.getUnratio();
}
参考资料
redis缓存实现
https://www.jianshu.com/p/931484bb3fdc
自定义序列化和key前缀实现方式
https://www.jianshu.com/p/713069fbd889
https://blog.csdn.net/u012129558/article/details/80520693
自定义fastJson作为redis序列化方式
https://blog.csdn.net/b376924098/article/details/79820642