【任务12】数据统计与拦截:统计网站每日访问IP,PV,UV

本问题对应的项目链接:从0设计一个web项目

本次打卡要求:
1、在你的首页留一个小空间,展示今日与昨日的PV,IP,UV
2、提供三个接口,getPV, getIP, getUV,并且给出这三个接口的使用文档,就是你把我当作第三方,我想调用你这个接口。
3、简单说一下你是如何统计 pv, ip, 和 uv 的。

共以下 5 个回答

  • 豆子彩虹 普通 2022年11月1日 下午4:43

    接口文档

    getIp() getUv() getPv()
    请求方式get
    参数 startday 开始日期(String) endday 结束日期(String)
    开始日期和结束日期都为空时默认返回当天信息
    开始日期和结束日期有一项为空时返回不为空那天的信息
    日期格式为yyyy-MM-dd

    @GetMapping("/ip")
        public R getIp(@RequestParam(value = "startDay", required = false) String startDay, @RequestParam(value = "endDay", required = false) String endDay) throws ParamIllegalException {
            Long num = 0L;
            //如果没有接收到参数
            if (startDay == null && endDay == null) {
                LocalDate date = LocalDate.now();
                String ip = date + ":ip";
                num = redisTemplate.opsForSet().size(ip);
                //两个参数都接收到
            } else if (startDay != null && endDay != null) {
                List days = getDays(startDay, endDay);
                for (LocalDate localDate : days) {
                    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format);
                    String date = dateTimeFormatter.format(localDate) + ":ip";
                    Long size = redisTemplate.opsForSet().size(date);
                    num += size;
                }
            } else {
                LocalDate start = LocalDate.parse(startDay == null ? endDay : startDay, DateTimeFormatter.ofPattern(format));
                String date = start + ":ip";
                num = redisTemplate.opsForSet().size(date);
            }
            return R.ok().put("ip", num);
        }
    
        @GetMapping("/pv")
        public R getPv(@RequestParam(value = "startDay", required = false) String startDay, @RequestParam(value = "endDay", required = false) String endDay) throws ParamIllegalException {
            int num = 0;
            if (startDay == null && endDay == null) {
                LocalDate date = LocalDate.now();
                String pv = date + ":pv";
                String size = redisTemplate.opsForValue().get(pv);
                num = Integer.parseInt(size == null ? 0 + "" : size);
            } else if (startDay != null && endDay != null) {
                List days = getDays(startDay, endDay);
    
                for (LocalDate localDate : days) {
                    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format);
                    String date = dateTimeFormatter.format(localDate) + ":pv";
                    String size = redisTemplate.opsForValue().get(date);
                    num += Integer.parseInt(size == null ? 0 + "" : size);
                }
            } else {
                LocalDate start = LocalDate.parse(startDay == null ? endDay : startDay, DateTimeFormatter.ofPattern(format));
                String date = start + ":pv";
                String size = redisTemplate.opsForValue().get(date);
                num = Integer.parseInt(size == null ? 0 + "" : size);
            }
            return R.ok().put("pv", num);
        }
    
    
        @GetMapping("/uv")
        public R getUv(@RequestParam(value = "startDay", required = false) String startDay, @RequestParam(value = "endDay", required = false) String endDay) throws ParamIllegalException {
            Long num = 0L;
            if (startDay == null && endDay == null) {
                LocalDate date = LocalDate.now();
                String uv = date + ":uv";
                num = redisTemplate.opsForSet().size(uv);
            } else if (startDay != null && endDay != null) {
                List days = null;
                days = getDays(startDay, endDay);
                for (LocalDate localDate : days) {
                    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format);
                    String date = dateTimeFormatter.format(localDate) + ":uv";
                    Long size = redisTemplate.opsForSet().size(date);
                    num += size;
                }
            } else {
                LocalDate start = LocalDate.parse(startDay == null ? endDay : startDay, DateTimeFormatter.ofPattern(format));
                String date = start + ":uv";
                num = redisTemplate.opsForSet().size(date);
            }
            return R.ok().put("uv", num);
        }
    
        private List getDays(String startDay, String endDay) throws ParamIllegalException {
            LocalDate end = LocalDate.parse(endDay, DateTimeFormatter.ofPattern(format));
            LocalDate start = LocalDate.parse(startDay, DateTimeFormatter.ofPattern(format));
            if (start.isAfter(end)) {
                throw new ParamIllegalException();
            }
            long numOfDays = ChronoUnit.DAYS.between(start, end);
    
            return Stream.iterate(start, date -> date.plusDays(1))
                    .limit(numOfDays)
                    .collect(Collectors.toList());
        }
    

    统计方式

    使用redis进行存储,其中uv用键值对存储,ip和pv用set存储
    在通过登陆拦截后,判断请求的地址是否为主页地址,如果是就添加到redis数据库中

    String uri = request.getRequestURI();
                        if ("/FromZerotoExpert/index.html".equals(uri)) {
                            //PV+1
                            LocalDate date = LocalDate.now();
                            String pv = date + ":pv";
                            redisTemplate.opsForValue().increment(pv);
                            //维护当日登陆的用户set
                            String uv = date + ":uv";
                            redisTemplate.opsForSet().add(uv, userId);
                            //维护当日登陆的ip
                            String ip = date + ":ip";
                            String userIp = request.getRemoteAddr();
                            System.out.println(userIp);
                            redisTemplate.opsForSet().add(ip, userIp);
    

    查看其他同学的打卡记录,发现还是存在一些问题,记录下来并进行改进
    1. 访问页面时,即使不登录uv的数量也增加
    2. redis数据结构hyperloglog
    3. 数据持久化问题
    GitHub地址:https://github.com/lei342345795/FromZerotoExpert

    0 赞同 0 条回复
  • lucca 普通 2022年10月24日 下午10:05

    获取网站PV、UV、IP统计 接口文档

    GET /monitor/getPV
    GET /monitor/getUV
    GET /monitor/getIP

    提供格式化日期,获取当天流量统计情况

    请求参数

    参数名 形式 类型 必填 长度 说明
    Authorization header string 是 登录token(请求头携带)
    dateTime param string 是 要获取流量的日期(格式:yyyy-MM-dd)

    返回数据

    网站UV、PV、IP数

    结果示例

    {
      "data": 20,
      "total": null,
      "success": true,
      "code": 200,
      "message": "服务运行成功"
    }
    

    如何统计

    • PV: 前端每刷新一个页面,就调一次自增接口
    • IP: request 获取ip, 使用 redis hyperloglog 统计 (存在一定误差)
    • UV:统计用户sessionId, 使用 redis hyperloglog 统计 (存在一定误差)

    github 地址(点击即可)

    0 赞同 0 条回复
  • 小肸 永久会员 2022年10月20日 下午8:07

    统计网站的IP,PV,UV

    IP:独立ip地址。一天之内同一个ip地址多次访问网站,只记为1。
    PV:页面的浏览次数。用户每刷新或打开一次页面,记为1次。
    UV:独立访问者。一天之内同一个访客的多次访问,记为1次。
    开始网站访问数据统计前,我还是先填下之前留下的一些坑,
    对于之前的登录拦截实现,我是用后端的一个接口isLogin结合前端的路由拦截(前端指定的路由跳转都会去向isLogin这个接口去请求看用户是否位于登录态,准确的来说,前端所做的工作叫路由守卫。)判断用户是否具有跳转资格,这样的确可以实现这个功能,但后端还是存在安全隐患,因为用户可以不通过我们的前端代码,而是直接写一个请求跳过isLogin接口直接向后端指定接口发送请求做一些操作,虽然那个接口可能有数据验证等,但不能确保用户不合规范的操作可能对系统数据造成一定影响。我之所以采用这种方法进行登录拦截,其根本思想就是想拦截所有发送给后端的请求再做验证,但我是在前端实现,有些不安全。做登录拦截时对拦截器的概念还不清楚,为什么有拦截器的存在,拦截器就是面向切面编程,AOP的思想,想一想,比如处理每一个请求前,都要做统一权限验证,处理每一个请求后都要做同一的资源清理,如果不用拦截器,也可以,但你需要在处理每一个请求前后加上统一的代码,就会有大量的代码冗余,统计网站的数据功能可能也会借助拦截器实现。之前我做登录拦截时可能是想自己实现一个拦截器,却在前端实现,Spring给我们提供了拦截器,我们只需继承并修改它就行了。实现的话:需要一个拦截器和一个配置类,将拦截器注册就行了。

    org.springframework.web.util包下的WebUtils类的getCookie()方法可以帮你找到请求中的指定cookie,就不需要自己写一个方法实现。

    自定义拦截器类MyInterceptor

    @Component
    public class MyInterceptor implements HandlerInterceptor {
        @Resource
        private RedisTemplate redisTemplate;
        //返回值为false,则说明禁止访问
        //返回值为true,则说明允许访问
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            countWebsiteData(request, response);
            //不拦截路径(登录路径等等)
            List asList = Arrays.asList("/fromzerotoexpert/register", "/fromzerotoexpert/login", "/fromzerotoexpert/FromZerotoExpert");
            String uri = request.getRequestURI();
            //1.设置放行路径
            if(asList.contains(uri)){
                return true;
            }
            // 设置输出流编码格式,支持中文
            response.setHeader("Content-type", "text/html;charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            if (getCookie(request, "accountId") != null) {
                Cookie cookie = getCookie(request, "accountId");
                String accountId = cookie.getValue();
                String sessionId = (String) redisTemplate.opsForList().index(accountId, 0);
    //            if (sessionId == null) {
    //                response.getWriter().write(JSONObject.toJSONString(ResultVOUtil.fail(401,"请先登录")));
    //                return false;
    //            }
                if (sessionId.equals(request.getSession().getId())) {
                    return true;
                } else {
                    response.getWriter().write(JSONObject.toJSONString(ResultVOUtil.fail(401,"你的账号已在其它地方登录")));
                    return false;
                }
            }
    //        System.out.println(JSONObject.toJSONString(ResultVOUtil.fail(401,"请先登录")));
            response.getWriter().write(JSONObject.toJSONString(ResultVOUtil.fail(401,"请先登录")));
            System.out.println("方法执行前.......");
            return false;
        }
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println("方法执行后.......");
        }
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println("清理.......");
        }
        // 更新网站当日的访问数据并向前端发送通知
        private void countWebsiteData(HttpServletRequest request, HttpServletResponse response) {
            // 当前日期 (只有日期没有时间)
            String curDate = LocalDate.now().format(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
            if (redisTemplate.opsForHash().get(curDate, "ip") == null) {
                redisTemplate.opsForHash().put(curDate, "ip", 0);
            }
            if (redisTemplate.opsForHash().get(curDate, "uv") == null) {
                redisTemplate.opsForHash().put(curDate, "uv", 0);
            }
            if (redisTemplate.opsForHash().get(curDate, "pv") == null) {
                redisTemplate.opsForHash().put(curDate, "pv", 0);
            }
            int newIpCount = (int) redisTemplate.opsForHash().get(curDate, "ip");
            int newUvCount = (int) redisTemplate.opsForHash().get(curDate, "uv");
            int newPvCount = (int) redisTemplate.opsForHash().get(curDate, "pv");
            // 调用IPUtil类的getIpAdress()方法获取请求真正的ip地址
            String curIp = IPUtil.getIpAdrress(request);
            // 如果是个新的ip地址,更新ip集合和newIp的值
            if (!redisTemplate.opsForSet().isMember("ip", curIp)) {
                redisTemplate.opsForSet().add("ip", curIp);
                newIpCount ++;
            }
            // 获取当前时间到第二天凌晨时间的秒数
            long targetSeconds = MyTimeUtil.getNowToNextDaySeconds();
            // 获取请求中Cookie键为UID的值
            if (getCookie(request, "UID") == null) {
                newUvCount ++;
                Cookie cookie = new Cookie("UID", "UID");
                cookie.setMaxAge((int) targetSeconds);
                response.addCookie(cookie);
            }
            newPvCount ++;
            redisTemplate.opsForHash().put(curDate, "ip", newIpCount);
            redisTemplate.opsForHash().put(curDate, "uv", newUvCount);
            redisTemplate.opsForHash().put(curDate, "pv", newPvCount);
            // 把过期时间设置长一些,以便定时统计
            redisTemplate.expire(curDate, targetSeconds + 5 * 60, TimeUnit.SECONDS);
            WebsiteDataForm websiteDataForm = new WebsiteDataForm();
            websiteDataForm.setDate(curDate);
            websiteDataForm.setIp(newIpCount);
            websiteDataForm.setUv(newUvCount);
            websiteDataForm.setPv(newPvCount);
            // 向前端发送新的序列化后的网站访问数据
            Websocket.sendMessage(JSONObject.toJSONString(websiteDataForm));
        }
    }
    

    配置类CrosConfig(因为我的项目是前后端分离,所以需要跨域,也要在这个类中注册我们自定义的拦截器类)

    @Configuration
    public class CrosConfig extends WebMvcConfigurationSupport {
    
        @Autowired
        private MyInterceptor myInterceptor;
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOrigins("*")
                    .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                    .allowCredentials(false)
                    .maxAge(3600)
                    .allowedHeaders("*");
        }
        //将自定义的拦截器注册
        @Override
        protected void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(myInterceptor).addPathPatterns("/**");
        }
    }
    

    怎么统计当天的ip、uv、pv并持久化

    借助redis,为什么使用redis,redis的数据主要在缓存中,查询速度快,不使用mysql,因为mysql的数据主要在磁盘中。
    题外话,上网查查mysql和redis的区别和使用场景

    查询速度:mysql的数据主要存储在磁盘中,虽然mysql使用了先写日志的策略和位于内存中的buffer pool来加快数据的操作,但还是比不上redis对数据的操作,因为redis的数据在内存中,redis只有在数据持久化的时候才会使用磁盘。对于网站的ip、uv、pv,它们访问修改比较频繁,并且数据的大小不大,可以放在内存中,加快速度。
    区别:mysql的数据是以表的数据来存储的,属于关系型数据库,redis的数据是以键值对的形式存储,属于非关系数据库,redis的键只能是字符串类型,但值有多种类型,主要的五大类型是字符串、哈希、列表、集合、有序集合。

    怎么用redis实现:
    一个键值对:键类型为字符串类型,键值为当天日期;值类型为哈希类型,值的值为三个哈希对:、、。过期时间设置为当天过期。
    一个键值对:键值为ip,值类型为集合,值的值为客户端ip地址。
    在拦截器中实现:每有一个请求被拦截,pv就加一,然后去键为ip的集合中看看ip是否存在,不存在ip就加一,并把ip加入到集合中。
    持久化:
    设置一个定时任务,定时执行,比如凌晨就把当天统计的网站访问数据写入到mysql中,就行了。因为往期的网站访问数据都在mysql表中,就好操作了。

    你可以定义一个实体类,然后根据业务实现接口,比如查哪一天的网站访问数据(ip、uv、pv),提供给外部。

    怎么展示当天的网站浏览数据:

    方法一:前端定时请求。简单,当增加了服务器的压力,比如网站的访问数据一直没变化,你却一直给网站发请求要它的访问数据。
    方法二:因为当天的网站数据是实时变化,这个场景非常适合用设计模式中的观察者模式(当被观察者自身数据发送变化时,被观察者主动告知观察者新的数据),当被观察者是服务器,服务器需要主动向客户端推送信息,这时需要用到WebSocket全双工通信协议。如果你想获得当天网站的实时访问数据,却不想去网站,网站可以提供一个接口,你访问这个接口,网站就可以把你注册为观察者,网站就可以直接向你告知自身访问数据的变化。

    代码有些杂,放上WenSocket协议的后端实现(一个WebSocket类)。

    @ServerEndpoint("/websocket/{username}")
    @Slf4j
    @Component
    public class Websocket {
      //静态变量,用来记录当前在线连接数。设计为安全的
      private static int onlineCount = 0;
      //concurrentHashMap分段锁,用来存放每个客户端对应的Websocket对象。
      private static Map clients = new ConcurrentHashMap();
      //与某个客户端的连接会话,需要通过它来给客户端发送数据
      private Session session;
      private String username;
    
    /**
       * 连接建立成功调用的方法
       * @param username
       * @param session
       */
      @OnOpen
      public void onOpen(@PathParam("username") String username, Session session) {
          this.username = username;
          this.session = session;
          Websocket.onlineCount++;
          log.info("有一连接进入!当前在线人数为" + onlineCount);
          clients.put(username, this);
      }
    
    /**
       * 连接关闭调用的方法
       */
      @OnClose
      public void onClose() {
          clients.remove(username);
          Websocket.onlineCount--;
          log.info("有一连接关闭!当前在线人数为" + onlineCount);
    
    }
    
    /**
       * 收到客户端消息后调用的方法
       * @param message
       */
      @OnMessage
      public void onMessage(String message) {
          System.out.println("收到客户端的消息"+message);
          sendMessage(message);
      }
    
    @OnError
      public void onError(Session session, Throwable throwable) {
          log.error("WebSocket发生错误:" + throwable.getMessage());
      }
    
    public static void sendMessage(String message) {
          // 向所有连接websocket的客户端发送消息
          // 可以修改为对某个客户端发消息
          for (Websocket item : clients.values()) {
              item.session.getAsyncRemote().sendText(message);
          }
      }
    }
    

    之前项目部署有些问题,记录一下,就是因为浏览器的同源策略,协议、域名、端口三者之前只要有一个不同就属于不同源,需要跨域。比如后端本机启动启动端口为8080,协议为http,后端就是http://localhost:8080,而前端启动端口为8090,也是本机启动,协议为8090,前端就是http://localhost:8090,这时前后端属于不同源,解决这个问题可以在后端配置跨域请求,也可以在Vue中使用代理,(因为我是Vue中用axios工具发送http请求,需要设置axios.defaults.withCredentials=true,让axios发送请求的每一次带默认携带服务器返回的cookie,设置了axios.defaults.withCredentials=true还需在后端配置允许接受指定前端端口携带的cookie)。uv的统计修改了下,就看每次请求中有没有UID的cookie,没有的话,统计数加一,返回一个当天过期的UID,UID的cookie值就为UID。
    GitHub地址:https://github.com/shuaishuaixi03/FromZerotoExpert/tree/dev
    服务器地址:http://47.98.198.2:8089/

    0 赞同 2 条回复
    • shuaidi 永久会员 @ 小肸 2022年10月20日 下午11:56

      用谷歌浏览器一直没登录上,必须需要自己该浏览器配置吗?

      另外就是,你的统计有一些建议:
      1、为了防止定时任务没有统计到数据,写入数据库中,过期时间可以多一点勒。
      2、另外就是,UV不一定就要用户登录哦,比如一些网站在统计UV时,即使用户没登录也是可以统计的哦。

    • 小肸 永久会员 @ shuaidi 2022年10月21日 下午3:24

      好像是我实现前后端websocket协议连接和跨域的一些问题,我再改改,今天用其它电脑测试下,不能登录,我的电脑好像可以登录

  • Second(S) 普通 2022年10月12日 下午12:08

    1.PV,UV,IP的展示:http://123.57.142.33:8088/FromZerotoExpert/
    2.接口的使用文档
    ①getIP
    功能:统计当天的IP数。
    参数:无
    返回值类型:String
    说明:此接口利用redis数据库将IP数据实时展示在前端页面。无需操作,定时任务自动调用展示。
    ②getUV
    功能:统计当天的UV数。
    参数:无
    返回值类型:String
    说明:此接口利用redis数据库将UV数据实时展示在前端页面。无需操作,定时任务自动调用展示。
    ③getPV
    功能:统计当天的PV数。
    参数:无
    返回值类型:String
    说明:此接口利用redis数据库将PV数据实时展示在前端页面。无需操作,定时任务自动调用展示。
    ④getRangeWebInformation
    功能:统计n天前的IP, PV, UV数据总量。范围查询。
    参数类型:Integer
    参数含义:n天前的数据
    返回值类型:Map<String, String>
    说明:此接口利用前端页面的输入框和查询按钮进行n天前的数据总量统计查询。返回结果{“ip”:ip, “uv”:uv, “pv”:pv}。
    3.实现方式
    ①统计PV
    设置PV拦截器,当请求经过拦截器时,将redis记录的pv数加一,其中redis记录pv的key为web:pv:+当前时间(年月日),redis记录的key为PV数,初始为0,每当请求被拦截时加一。使用的redis数据类型为string类型。

    @Component
    public class PvInterceptor implements HandlerInterceptor {
        public static final String PREFIX_KEY_PV = "web:pv:";
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            try {
                Date date = new Date(System.currentTimeMillis());
                String key = PREFIX_KEY_PV + date.toString();
                if(!stringRedisTemplate.hasKey(key)){
                    stringRedisTemplate.opsForValue().set(key,"0");
                }
    
                Integer pv = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
                ++pv;
                stringRedisTemplate.opsForValue().set(key,pv + "");
                stringRedisTemplate.expire(key, 60 * 60 * 27, TimeUnit.SECONDS);
            }finally {
                return true;
            }
        }
    }
    

    ②统计UV
    设置UV拦截器,当请求被拦截时,给客户端设置一个Cookie当作唯一标识,名为pv_cookie_id,值是一个唯一的字符串。并通过当前时间,计算出到今天23:59:59的剩余时间,当作Cookie的持久化存储时间,当同一客户端的请求在当天再次访问时,Cookie检测存在,不进行redis的存储,只有Cookie不存在时,才将Cookie的唯一字符值传入到redis存储,当第二天时,所有Cookie都失效,UV会被重新统计。其中redis记录uv的key为web:uv:+当前时间(年月日),redis记录的值为Cookie中存储的唯一标识,redis使用的数据类型为HyperLogLog。

    @Component
    public class UvInterceptor implements HandlerInterceptor {
        public static final String PREFIX_KEY_UV = "web:uv:";
        public static final String PV_COOKIE_NAME = "pv_cookie_id";
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            try {
                Date date = new Date(System.currentTimeMillis());
                String key = PREFIX_KEY_UV + date.toString();
                String cookieValue = CookieUtils.getCookieValue(request, PV_COOKIE_NAME);
                if(null == cookieValue){
    
                    String c_value = IdUtil.simpleUUID();
                    CookieUtils.setCookie(request,response,PV_COOKIE_NAME,c_value,
                            CookieDeathUtils.getCookieDeath());
    
                    stringRedisTemplate.opsForHyperLogLog().add(key, c_value);
                    stringRedisTemplate.expire(key, 60 * 60 * 24, TimeUnit.SECONDS);
                }else{
                    stringRedisTemplate.opsForHyperLogLog().add(key, cookieValue);
                    stringRedisTemplate.expire(key, 60 * 60 * 27, TimeUnit.SECONDS);
                }
            }finally {
                return true;
            }
    
        }
    }
    

    ③统计IP
    设置IP拦截器,当请求被拦截时,根据request解析用户ip,并在redis中进行存储,其中redis记录ip的key为web:ip:+当前时间(年月日),redis记录的值为用户ip,redis使用的数据类型为HyperLogLog。

    @Component
    public class IpInterceptor implements HandlerInterceptor {
        public static final String PREFIX_KEY_IP = "web:ip:";
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            try {
                Date date = new Date(System.currentTimeMillis());
                String key = PREFIX_KEY_IP + date.toString();
                String ip = IpUtils.getIpAddr(request);
    //            System.out.println("ip解析:" + ip);
                stringRedisTemplate.opsForHyperLogLog().add(key, ip);
                stringRedisTemplate.expire(key, 60 * 60 * 27, TimeUnit.SECONDS);
            }finally {
                return true;
            }
    
        }
    }
    

    ④注册拦截器
    注册拦截器需要注意,除了放行静态资源,还需要把一些重定向的中转接口,数据的查询接口,存储接口等放行,不然一些定时任务的触发会被拦截器拦截,使统计信息不够准确。

    4.持久化存储
    每天晚上23:59会执行定时任务把所有redis数据存储至mysql数据库,对于当天的数据查询均在redis中操作,对于以前的数据查询,均在mysql中进行操作。

    gitHub地址:https://github.com/Second6s9/FromZerotoExpert/tree/dev
    gitHub防覆盖地址:https://github.com/Second6s9/FromZerotoExpert/tree/day12
    服务器地址:http://123.57.142.33:8088/FromZerotoExpert/

    0 赞同 2 条回复
  • 秊 永久会员 2022年10月9日 下午10:24
    1. 点击–>文档
    2. IP: 通过 HttpServeletRequest 获取;redis中存:fU:ip:当前日期 = ip;使用 HyperLogLog

    PV: 用户每次刷新网页 + 1; redis中存:fU:pv:当前日期 = 0; 使用 字符串

    UV: 通过用户浏览器中的 Cookie:【fzteUser = 用户账号】来确认一个访客;redis中存:fU:uv:当前日期 = 用户账号;使用 HyperLogLog

    如何将这些数据保存起来?

    –> 就存在redis中,设置有效期为 30 天

    –> 定时任务:每日0点以后将 redis 中的这些数据转移到 mysql 中,同时 mysql 删除30天以前的数据 (10.10升级到了这种):使用了 Spring 中的定时任务功能

    部分实现代码如下:

            
            // 拦截器中部分代码
            // 验证登录凭证
                if (request.getCookies() != null && isValidLoginCert(jedis, request, fzteUser)) {
    
                    // 记录PV
                    jedis.incr("fU:pv:" + nowTime);
                    // 记录IP
                    String ip = IpUtils.getIpAddr(request);
                    jedis.pfadd("fU:ip:" + nowTime, ip);
                    // 记录UV
                    if (fzteUser != null) jedis.pfadd("fU:uv:" + nowTime, fzteUser.getValue());
    
                    return true;
                }
    
         // controller层,其它两个类似
         /**
         * 获取访问的IP数
         * @return null - 该天数据不存在
         */
        @GetMapping("/GetIP")
        @ResponseBody
        public String getIP(String date) {
            Jedis jedis = null;
            jedis = UserService.jedisPool.getResource();
    
            long res = jedis.pfcount("fU:ip:" + date);
            if (res == 0) return null;
    
            jedis.close();
            return Long.toString(res);
        }
    

    前端会的很少,所有只能写出下面这种代码:

    function showIPPVUV() {
        let time = new Date();
        let time2 = new Date();
        let today = getNowFormatDate("", time);
        let yesterday = getNowFormatDate("", new Date(time.setDate(time.getDate() - 1)));
    
        let htmlString = "" +
            "" +
            "
    DATEIPPVUV
    " + getNowFormatDate("-", time2) + "" + getIP(today) + ""+ getPV(today) + "" + getUV(today) + "
    " + getNowFormatDate("-", new Date(time2.setDate(time2.getDate() - 1))) + "" + getIP(yesterday) + ""+ getPV(yesterday) + "" + getUV(yesterday) + "
    "; console.log(htmlString); document.getElementById("insertIPUVPV").innerHTML = htmlString; } function getIP(date) { let ans = ""; $.ajax({ url: '/GetIP', method: 'get', data: { date : date }, async: false, dataType: "json", success: function (res) { ans = String(res); } }) return ans; }

    ps: 这个 PV 帮我找到了jedis连接池一直不够的原因:拦截器中忘记屏蔽【发送心跳信息】和【显示在线人数】两个接口了,每次访问这两个接口还要用 jedis 验证一遍唯一登录凭证,感觉没有必要;屏蔽之后最大连接数 100 就稳稳的了。

    服务器链接:http://124.223.184.187:8088/FromZerotoExpert
    Github仓库链接:https://github.com/jiangyklala/FromZerotoExpert/tree/dev

    0 赞同 4 条回复

# 回答此问题

后才能回答