目录
为什么要用缓存
- 如果用户 A 打开 APP,进入到了秒杀商品的详情页,那这个商品数据我们会先去数据库查询,然后返回给客户端。
- 因为有大量用户短时间内进入到了详情页,所以可以把活动列表缓存起来,直接读缓存就可以了。
- 那下次再查询商品时,直接去缓存查询就可以了。如果秒杀商品下架了,缓存的数据不会用到了,就把缓存删掉就可以了。
代码实现
自定义注解
Cache:
package com.huing.blog.common.cache;
import java.lang.annotation.*;
/**
* @author huing
* @create 2022-06-27 11:22
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
//过期时间
long expire() default 1 * 60 * 1000;
//缓存标识 key
String name() default "";
}
aop逻辑实现
就是找到加了@cache注解的方法,把返回值和相关参数属性放到redis缓存里。
package com.huing.blog.common.cache;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huing.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.Duration;
/**
* aop 定义一个切面,切面定义了切点和通知的关系
*
* 就是找到加了@cache注解的方法,把返回值和相关参数属性放到redis缓存里
*
* @author huing
* @create 2022-06-27 11:23
*/
@Aspect
@Component
@Slf4j
public class CacheAspect {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private RedisTemplate<String, String> redisTemplate;
//切点,注解加在那里,那里就是切点
@Pointcut("@annotation(com.huing.blog.common.cache.Cache)")
public void pt(){}
//环绕通知,在方法的前后进行增强
@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
try {
Signature signature = pjp.getSignature();
//类名
String className = pjp.getTarget().getClass().getSimpleName();
//调用的方法名
String methodName = signature.getName();
Class[] parameterTypes = new Class[pjp.getArgs().length];
Object[] args = pjp.getArgs();
//参数
String params = "";
for(int i=0; i<args.length; i++) {
if(args[i] != null) {
params += JSON.toJSONString(args[i]);
parameterTypes[i] = args[i].getClass();
}else {
parameterTypes[i] = null;
}
}
if (StringUtils.isNotEmpty(params)) {
//加密 以防出现key过长以及字符转义获取不到的情况
params = DigestUtils.md5Hex(params);
}
Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
//获取Cache注解
Cache annotation = method.getAnnotation(Cache.class);
//缓存过期时间
long expire = annotation.expire();
//缓存名称
String name = annotation.name();
//先从redis获取
String redisKey = name + "::" + className+"::"+methodName+"::"+params;
String redisValue = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(redisValue)){
log.info("走了缓存~~~,{},{}",className,methodName);
Result result = JSON.parseObject(redisValue, Result.class);
return result;
}
//调用方法
Object proceed = pjp.proceed();
redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));
log.info("存入缓存~~~ {},{}",className,methodName);
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return Result.fail(-999,"系统错误");
}
}
Controller控制层使用
ArticleController:
/**
* 最热文章
* @return
*/
@PostMapping("hot")
@Cache(expire = 5 * 60 * 1000,name = "hot_article")
public Result hotArticle(){
return articleService.hotArticle(limit);
}
实现效果
Sprting Cache
Spring Cache简介
Spring Cache 是 Spring 提供的一整套的缓存解决方案。虽然它本身并没有提供缓存的实现,但是它提供了一整套的接口和代码规范、配置、注解等,这样它就可以整合各种缓存方案了,比如 Redis、Ehcache,我们也就不用关心操作缓存的细节。
Cache接口它包含了缓存的各种操作方式,同时还提供了各种xxx
Cache缓存的实现,比如 RedisCache 针对Redis,EhCacheCache 针对 EhCache,ConcurrentMapCache 针对 ConCurrentMap。
Spring Cache使用效果
每次调用某方法,而此方法又是带有缓存功能时,Spring 框架就会检查指定参数
的那个方法是否已经被调用过,如果之前调用过,就从缓存中取之前调用的结果;如果没有调用过,则再调用一次这个方法,并缓存结果,然后再返回结果,那下次调用这个方法时,就可以直接从缓存中获取结果了。
缓存注解
注解 | 描述 |
@Cacheable | 在调用方法前,首先去缓存中找方法的返回值,如果能找到,则返回缓存的值,否则就执行这个方法,并将返回值放到缓存中。 |
@CachePut | 在方法调用前不会去缓存中找,无论如何都会执行方法,执行后将缓存数据放入缓存中。 |
@CacheEvict | 清理缓存中的一个或多个记录 |
@Caching | 能够同时应用多个缓存注解 |
@CacheConfig | 在类级别共享相同的缓存的配置 |
Cacheable注解
- 标记在一个方法上,也可以标记在一个类上
- 缓存标注对象的返回结果,标注在方法上缓存该方法的返回值,标注在类上缓存该类所有方法的返回值
- value缓存名称,可以有多个
- key缓存的key规则,可以使用springEL表达式,默认是方法参数组合
- condition缓存条件,使用springEL编写,返回true才缓存
案例:
//对象
@Cacheable(value = {"product"},key = "#root.methodName")//分页
@Cacheable(value = {"product_page"}, key = "#root.methodName + #page + '_' + #size")
在线博客系统SpringCache使用
在本项目中使用Spring Cache
编码实现
pop依赖
<!-- cache-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
application.properties配置文件
#cache 配置文件指定缓存类型
spring.cache.type=redis
启动类开启缓存注解
package com.huing.blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
/**
* @author huing
* @create 2022-07-03 10:01
*/
@SpringBootApplication
@EnableCaching
public class BlogApp {
public static void main(String[] args) {
SpringApplication.run(BlogApp.class,args);
}
}
CacheConfig配置文件
package com.huing.blog.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.time.Duration;
/**
* Cache注解类,配置了过期时间和序列化
*
* @Author huing
* @Create 2022-07-15 15:42
*/
@Configuration
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager1Minute(RedisConnectionFactory connectionFactory){
RedisCacheConfiguration config = instanceConig(60L);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}
@Bean
@Primary // 默认的,没有指定采用默认的
public RedisCacheManager cacheManager1Hour(RedisConnectionFactory connectionFactory){
RedisCacheConfiguration config = instanceConig(3600L);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}
@Bean
public RedisCacheManager cacheManager1Day(RedisConnectionFactory connectionFactory){
RedisCacheConfiguration config = instanceConig(3600 * 24L);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}
private RedisCacheConfiguration instanceConig(long ttl) {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
//去掉各种@JsonSerialize注解的解析
objectMapper.configure(MapperFeature.USE_ANNOTATIONS,false);
//只针对非空的值进行序列化
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
//将类型序列化到属性json字符串中
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(ttl))
.disableCachingNullValues() //禁止缓存null的值
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
}
}
缓存注解实现
例如,我们想在对newArticle(最新文章)接口实现SpringCache缓存,直接添加Cacheable注解即可:
/**
* 最新文章
*
* @return
*/
@PostMapping("new")
@Cacheable(value = {"newArticle"}, key = "#root.methodName",cacheManager = "cacheManager1Minute") //这里我们设置过期时间为1分钟,默认为一小时
public Result newArticle() {
return articleService.newArticle(limit);
}
效果展示
前端发送对应请求之后,redis中:
可以发现,缓存的名称是newArticle,缓存的key为方法名称newArticle,并且当前过期时间为34秒(设置的为60秒)。
SpringCache详解请参考: