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的过期策略

我们可以给每一篇文文章设置过期时间,然后去监听,当时间过期后就将文章的浏览量同步到数据库中

具体可以参考[Redis过期时间监听处理]([CodeWorld | 收废铁](CodeWorld-Cloud-Shop 订单过期处理详解)

使用定时任务

可以设置一个定时任务来跑我们的数据,例如在晚上的凌晨几点,这个时候一般请求会小很多,这个时候就可以同步我们的数据了

具体实现
 /**
     * 定时更新问题浏览量到数据库中
     * 每天凌晨两点跑一次
     */
    @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,查询出文章,将其同步到数据库中

是不是很简单呢?还没实现?赶快行动起来吧

好了,本次的技术解析就到这里了?如果觉得不错的话,点亮一下小星星codeworld-cloud-shop
只看不点,不是好孩子哦!!

欢迎加入QQ群(964285437)

QQ群

欢迎加入公众号

公众号

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐