目录
1.统一缓存
1.1 统一缓存的好处:
- 响应速度提升:相比直接调用底层高流量的基础服务,调用缓存服务接口的系统响应时间大大减少
(内存的访问速度远远大于磁盘的访问速度 ,1000倍起) - 统一缓存,降低接入成本:一部分业务场景下可以直接标识统一缓存服务注释,便可以快速开启缓存功能,而不用再对接底层的多个子系统,极大地降低了各个业务线的接入成本。
1.2 编码
定义注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
//高速缓冲存储器
public @interface Cache {
//过期时间
long expire() default 1 * 60 * 1000;
//缓存标识
String name() default "";
}
定义切面
package com.pjp.blog.common.cache;
import com.alibaba.fastjson.JSON;
import com.pjp.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 定义一个切面,切面定义了切点和通知的关系
@Component
@Aspect
@Slf4j
public class CacheAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
//自己定义的注解标在哪儿 那儿的方法就是切点
@Pointcut("@annotation(com.pjp.blog.common.cache.Cache)")
public void pt(){}
//环绕通知
@Around("pt()") //"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);
return JSON.parseObject(redisValue, Result.class);
}
//触发目标方法的执行(被代理的对象的方法)
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,"系统错误");
}
}
使用:
@PostMapping("hot")
@Cache(expire = 5 * 60 * 1000,name = "hot_article")
public Result hotArticle(){
int limit = 5;
return articleService.hotArticle(limit);
}
2.缓存一致性问题
之前在文章列表读取,最新文章等接口的时候我们加了缓存,但是加了缓存会有一些问题,当我们修改或者用户浏览了文章,那么最新的修改和文章的浏览数量无法及时的更新,那么应该怎么做呢?
这里采用了RocketMQ来解决这个问题。
2.1.安装rocketMQ
2.1.1.下载镜像
docker search rocketmq
docker pull rocketmqinc/rocketmq
docker pull pangliang/rocketmq-console-ng
2.1.2.创建namesrv
创建需要挂载的目录
mkdir -p /docker/rocketmq/namesrv/logs /docker/rocketmq/namesrv/store
下面的-v对应的就上面创建的地址
docker run -d \
--restart=always \
--name rmqnamesrv \
--privileged=true \
-p 9876:9876 \
-v /docker/rocketmq/namesrv/logs:/root/logs \
-v /docker/rocketmq/namesrv/store:/root/store \
-e "MAX_POSSIBLE_HEAP=100000000" \
rocketmqinc/rocketmq \
sh mqnamesrv
2.1.3.创建broker
创建3个文件目录
mkdir -p /docker/rocketmq/broker/logs \
/docker/rocketmq/broker/store \
/docker/rocketmq/conf
创建broker.conf文件
touch /docker/rocketmq/conf/broker.conf
vim /docker/rocketmq/conf/broker.conf
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
brokerIP1 = 47.113.184.34
docker run -d \
--restart=always \
--name rmqbroker5 \
--link rmqnamesrv:namesrv \
--privileged=true \
-p 10911:10911 \
-p 10909:10909 \
-v /docker/rocketmq/broker/logs:/root/logs \
-v /docker/rocketmq/broker/store:/root/store \
-v /docker/rocketmq/conf/broker.conf:/opt/rocketmq-4.4.0/conf/broker.conf \
-e "NAMESRV_ADDR=namesrv:9876" \
-e "MAX_POSSIBLE_HEAP=200000000" \
rocketmqinc/rocketmq \
sh mqbroker -c /opt/rocketmq-4.4.0/conf/broker.conf
2.1.4.创建RocketMQ-console
docker run -d \
--restart=always \
--name rmqadmin \
-e "JAVA_OPTS=-Drocketmq.namesrv.addr=47.113.184.34:9876 \
-Dcom.rocketmq.sendMessageWithVIPChannel=false" \
-p 7777:8080 \
pangliang/rocketmq-console-ng
2.1.5.查询启动状态
docker ps -a
2.1.6.浏览器预览
3.集成
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
添加对应的配置:
#rockermq配置
rocketmq.name-server=47.113.184.34:9876
rocketmq.producer.group=blog_group
代码:
//缓存一致性
if(isEdit){
//发送一条消息给rocketmq 当前文章更新了 更新一下缓存
ArticleMessage articleMessage = new ArticleMessage();
articleMessage.setArticleId(article.getId());
rocketMQTemplate.convertAndSend("blog-update-article",articleMessage);
}
消费者加上注释监听消息
package com.pjp.blog.service.mq;
import com.alibaba.fastjson.JSON;
import com.pjp.blog.service.ArticleService;
import com.pjp.blog.vo.ArticleMessage;
import com.pjp.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Set;
@Slf4j
@Component
@RocketMQMessageListener(topic = "blog-update-article", consumerGroup = "blog-update-article-group")
public class ArticleListener implements RocketMQListener<ArticleMessage> {
@Autowired
private ArticleService articleService;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void onMessage(ArticleMessage articleMessage) {
log.info("收到的消息:{}", articleMessage);
//做什么了,更新缓存
//1. 更新查看文章详情的缓存
Long articleId = articleMessage.getArticleId();
String params = DigestUtils.md5Hex(articleId.toString());
String redisKey = "view_article::ArticleController::findArticleById::" + params;
Result articleResult = articleService.findArticleById(articleId);
redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(articleResult), Duration.ofMillis(5 * 60 * 1000));
log.info("更新了缓存:{}", redisKey);
//2. 文章列表的缓存 不知道参数,解决办法 直接删除缓存
Set<String> keys = redisTemplate.keys("listArticle*");
keys.forEach(s -> {
redisTemplate.delete(s);
log.info("删除了文章列表的缓存:{}", s);
});
}
}