Hello大家好,好久没有写过文章了,前段时间的一直忙着毕业的事情,做毕业设计,没有好好的写过一篇文章,那么今天我们来讲什么呢?我们知道我们在观看文章的时候,文章都有什么浏览量、评论量、点赞量什么的,这些是怎么来设计的呢,又是怎么来保持真实性呢?今天我就来带大家了解了解怎么去实现一篇文章的浏览量统计?
欢迎指出错误点,不喜勿碰
前言
我们在开始之前呢?我相信你对Redis有一定的了解,基本类型都会基本的使用,主要就是String、List、Map、Set等的使用,如果你还不是很会、建议你先去学习一下Redis
准备
首先我们都知道,我们要去浏览一篇文章的详情时,我们都会去给文章增加浏览量、就会去更新数据表的某一个代表浏览量的字段信息,
那么我们就要进行update,如果访问量比较小、我们还可以接受、数据库还是一定的承受压力的,那么问题来了,每点一次我就去更新一次,这样数据库还是有点受不了哦,加上如果访问量很大的话,那这样数据库更受不住、甚至可能会让数据库直接宕机,宕机就完了呀!
直接背锅,这个月绩效直接清零
别慌,办法肯定比困难多
解决方案
第一种我们可以使用异步任务,每次访问文章的时候去调用异步任务,让异步任务去给我们对文章的浏览进行修改,这样可以稍微减轻数据库的压力。不过说的是稍微、如果请求还是过多还是有问题,这样就会不停的去创建线程去执行任务,这样会一直累积,累积、最终也直接崩盘。
第二种就使用到Redis的使用了,我们可以去定义环绕通知,让去监控我们的文章详情这个接口,只对这个接口有效,并且对ip进行限制、如果说这个ip已经访问过了,那这个ip将不在增加浏览量,最后我们使用定时任务去将文章的浏览量同步到数据库中。
上面说的很草率、说了这么多,我们应该怎么去实现呢?
要的,开干
添加依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Redis配置信息
redis:
host: xxxxx # IP地址
password: xxxxx # 密码
database: 0 # 仓库表
自定义注解
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface QuestionView {
/**
* 描述
*/
String description() default "";
}
这里我们定义了注解来实现我们Aop的监控,只有接口加上该注解才能被我们的Aop监控到
Redis工具类
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 删除缓存
*
* @param key
*/
public void delete(String key) {
this.redisTemplate.delete(key);
}
/**
* 添加ip到缓存
*
* @param key
* @param value
* @return
*/
public Long add(String key, String value) {
return this.redisTemplate.opsForHyperLogLog().add(key, value);
}
/**
* 获取总数
*
* @param key
* @return
*/
public Long size(String key) {
return this.redisTemplate.opsForHyperLogLog().size(key);
}
/**
* 获取全部的指定key
*
* @return
*/
public Set<String> getKeys(String pattern) {
return this.redisTemplate.keys(pattern);
}
/**
* 是否存在key
* @param key
* @return
*/
public boolean isExist(String key) {
return this.redisTemplate.hasKey(key);
}
/**
* 设置String 类型值
* @param key
* @param value
*/
public void setStringValue(String key, String value){
this.redisTemplate.opsForValue().set(key,value);
}
/**
* 设置String 类型值,并设置超时时间
* @param key
* @param value
*/
public void setStringValueAndTime(String key, String value, Long time){
this.redisTemplate.opsForValue().set(key,value,time, TimeUnit.SECONDS);
}
/**
* 获取string 类型 value
* @param key
* @return
*/
public String getStringValue(String key){
return (String)this.redisTemplate.opsForValue().get(key);
}
}
定义了几个基本常用的方法,可以帮助我们直接使用
IpUitls工具类
public class IpUtils {
//获取客户端IP地址
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknow".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (ip.equals("127.0.0.1")) {
//根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (Exception e) {
e.printStackTrace();
}
assert inet != null;
ip = inet.getHostAddress();
}
}
// 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ip != null && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
return ip;
}
}
定义Aop切面控制
@Aspect
@Slf4j
@Configuration
public class QuestionViewAspect {
@Autowired
private RedisUtil redisUtil;
private static final String QUESTION_KEY = "QUESTION_ID:";
/**
* 获取当前的ServletRequest
* @return
*/
protected HttpServletRequest servletRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
/**
* 定义切点
*/
@Pointcut("@annotation(com.codeworld.common.anno.QuestionView)")
public void questionViewPointCut(){}
/**
* 切入处理,环绕通知
* @param joinPoint
* @return
*/
@Around("questionViewPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
Object questionId = args[0];
Object obj = null;
try {
// 获取请求的ip
String ipAddr = IpUtils.getIpAddr(servletRequest());
log.info("请求Ip:{}",ipAddr);
// 设置存入的key
String key = QUESTION_KEY + questionId;
// 将存入到缓存中
Long count = this.redisUtil.add(key, ipAddr);
if (count == 0){
log.info("该Ip:{},访问量已经访问过了",ipAddr);
}
obj = joinPoint.proceed();
}catch (Exception e){
e.fillInStackTrace();
}
return obj;
}
}
主要是对用户请求过来的ip进行储存,将QUESTION_KEY + 文章的id作为key,ip作为value,这样每次请求都会进入这个切面控制器,对ip进行处理,如果说ip已经访问过一次,多次访问次数将不再增加
获取文章详情
QuestionVo questionVo = this.questionMapper.getQuestionDetail(id);
if (ObjectUtil.isEmpty(questionVo)) {
throw new CodeException("问题不存在");
}
// 从redis中获取问题的浏览量
Long viewCount = this.redisUtil.size(QUESTION_KEY + id);
questionVo.setViewCount(viewCount.intValue() + questionVo.getViewCount());
return ApiResponse.dataResponse("查询成功", questionVo);
Long viewCount = this.redisUtil.size(QUESTION_KEY + id);
主要是这个方法,通过key去查询该文章访问的ip数量,将其加到总的浏览量中,这样我们就不需要去操作数据库,进行修改信息了。
问题来了
既然我们可以保存浏览量到我们Redis缓存中了,那么万一我们浏览的文章数量越来越多,那么Redis就会越存越多,这样下去的话,Redis也会扛不住,Redis也会有内存限制的,我们应该怎么解决呢?
解决问题
使用Redis的过期策略
我们可以给每一篇文文章设置过期时间,然后去监听,当时间过期后就将文章的浏览量同步到数据库中
使用定时任务
可以设置一个定时任务来跑我们的数据,例如在晚上的凌晨几点,这个时候一般请求会小很多,这个时候就可以同步我们的数据了
具体实现
/**
* 定时更新问题浏览量到数据库中
* 每天凌晨两点跑一次
*/
@Scheduled(cron = "0 0 2 * * ?")
// @Scheduled(cron = "0/5 0/1 * * * ?")
@Transactional(rollbackFor = Exception.class)
public void updateQuestionView() {
log.info("开始执行问题浏览量入库");
long start = System.currentTimeMillis();
ScheduleLog scheduleLog = new ScheduleLog();
scheduleLog.setName("定时更新问题浏览量");
scheduleLog.setType((short) 2);
scheduleLog.setCreateTime(new Date());
scheduleLog.setUpdateTime(scheduleLog.getCreateTime());
// 获取全部的key
String pattern = "QUESTION_ID:*";
Set<String> keys = this.redisUtil.getKeys(pattern);
try {
for (String key : keys
) {
Long viewCount = this.redisUtil.size(key);
// 将key拆分
String[] split = key.split(":");
// 根据问题id获取
Question question = this.questionMapper.selectById(split[1]);
if (ObjectUtil.isEmpty(question)) {
throw new CodeException("问题不存在");
}
// 更改浏览量
question.setViewCount(viewCount.intValue() + question.getViewCount());
int count = this.questionMapper.updateById(question);
if (count == 0) {
throw new CodeException("问题浏览量更新失败");
}
// 删除key
this.redisUtil.delete(key);
}
long end = System.currentTimeMillis();
log.info("问题浏览量入库结束,耗时:{}", end - start);
scheduleLog.setStatus((short) 1);
scheduleLog.setRemark("任务执行成功");
scheduleLog.setTime(end - start);
} catch (Exception e) {
e.fillInStackTrace();
long end = System.currentTimeMillis();
scheduleLog.setStatus((short) 2);
scheduleLog.setRemark(e.getMessage());
scheduleLog.setTime(end - start);
} finally {
this.scheduleLogMapper.insert(scheduleLog);
}
}
查询出所有的key,然后获取到文章的id,查询出文章,将其同步到数据库中