• 15、网站统计数据


    UV(Unique Visitor)

    • 独立访客,需通过用户IP排重统计数据
    • 每次访问都要进行统计
    • HyperLogLog,性能好,且存储空间小
      DAU(Dail Active User)
    • 日活跃用户,需通过用户ID排重统计数据
    • 访问过一次,则认为其活跃(自定义)
    • Bitmap,性能好,且可以统计精准的结果

    1、RedisKey

        // 单日UV
        public static String getUVKey(String date) {
            return PREFIX_UV + SPLIT + date;
        }
    
        // 区间UV
        public static String getUVKey(String startDate, String endDate) {
            return PREFIX_UV + SPLIT + startDate + SPLIT + endDate;
        }
    
        // 单日活跃用户
        public static String getDAUKey(String date) {
            return PREFIX_DAU + SPLIT + date;
        }
    
        // 区间活跃用户
        public static String getDAUKey(String startDate, String endDate) {
            return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2、Service

    由于使用Redis存储数据,所以不需要访问DAO层,直接在Service层处理数据即可。
    DataService.java

    UV

    1、将指定IP计入UV
    通过new SimpleDateFormat(“yyyyMMdd”) 先指定日期格式

    @Service
    public class DataService {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");
    
        // 将指定的IP计入UV
        public void recordUV(String ip) {
            String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));
            redisTemplate.opsForHyperLogLog().add(redisKey, ip);
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2、统计:统计指定日期范围
    1)传入的日期参数,是Data类
    2)由于要统计日期范围内的数据,所以要生成一组Rediskey:List< String> keyList,其中用到了Calendar类对日期进行循环
    3)合并数据
    4)调用 redisTemplate.opsForHyperLogLog().size() 获得统计结果

        // 统计指定日期范围内的UV
        public long calculateUV(Date start, Date end) {
            if (start == null || end == null) {
                throw new IllegalArgumentException("参数不能为空!");
            }
    
            // 整理该日期范围内的key
            List<String> keyList = new ArrayList<>();
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(start);
            while (!calendar.getTime().after(end)) {
                String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime()));  // 单日UV
                keyList.add(key);
                calendar.add(Calendar.DATE, 1);                      // 日期+1
            }
    
            // 合并这些数据
            String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));  // 生成区间UV的key
            redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());
    
            // 返回统计的结果
            return redisTemplate.opsForHyperLogLog().size(redisKey);
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    DAU

    1、根据 userId 将指定用户计入DAU

        public void recordDAU(int userId) {
            String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date()));
            redisTemplate.opsForValue().setBit(redisKey, userId, true);
        }
    
    • 1
    • 2
    • 3
    • 4

    2、统计指定日期范围内的DAU
    与calculateUV() 方法类似,不同的是要对 区间范围内的数据进行OR操作,且connection.bitOp()要求传入RedisKey的Byte数组

        public long calculateDAU(Date start, Date end) {
            if (start == null || end == null) {
                throw new IllegalArgumentException("参数不能为空!");
            }
    
            // 整理该日期范围内的key
            List<byte[]> keyList = new ArrayList<>();	// 将RedisKey 转换成 byte数组
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(start);
            while (!calendar.getTime().after(end)) {
                String key = RedisKeyUtil.getDAUKey(df.format(calendar.getTime()));
                keyList.add(key.getBytes());   
                calendar.add(Calendar.DATE, 1);
            }
    
            // 进行OR运算
            return (long) redisTemplate.execute(new RedisCallback() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    String redisKey = RedisKeyUtil.getDAUKey(df.format(start), df.format(end));
                    connection.bitOp(RedisStringCommands.BitOperation.OR,     // OR运算
                            redisKey.getBytes(), keyList.toArray(new byte[0][0]));
                    return connection.bitCount(redisKey.getBytes());                 // 统计位数为true的个数
                }
            });
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    3、Controller

    @DateTimeFormat(pattern = “yyyy-MM-dd”) :Date start,是对日期参数的处理。
    return “forward:/data”:forward请求转发。声明该方法只能处理一半,还需要另外一个方法接着处理,请求转发到了 “/data” 路径,由于还是同一个请求,所以"/data"路径也要支持 RequestMethod.POST 请求。

    @Controller
    public class DataController {
    
        @Autowired
        private DataService dataService;
    
        // 统计页面
        @RequestMapping(path = "/data", method = {RequestMethod.GET, RequestMethod.POST})
        public String getDataPage() {
            return "/site/admin/data";
        }
    
        // 统计网站UV
        @RequestMapping(path = "/data/uv", method = RequestMethod.POST)
        public String getUV(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
                            @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model) {
            long uv = dataService.calculateUV(start, end);
            model.addAttribute("uvResult", uv);
            model.addAttribute("uvStartDate", start); // 把日期参数放进Model里,为了让页面有显示默认的值
            model.addAttribute("uvEndDate", end);
            return "forward:/data";
        }
    
        // 统计活跃用户
        @RequestMapping(path = "/data/dau", method = RequestMethod.POST)
        public String getDAU(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,
                             @DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model) {
            long dau = dataService.calculateDAU(start, end);
            model.addAttribute("dauResult", dau);
            model.addAttribute("dauStartDate", start);
            model.addAttribute("dauEndDate", end);
            return "forward:/data";
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    4、拦截器

    每次请求都要记录一下数据,所以这里使用的是拦截器

    @Component
    public class DataInterceptor implements HandlerInterceptor {
    
        @Autowired
        private DataService dataService;
    
        @Autowired
        private HostHolder hostHolder;  // 获取当前登录用户
    
    	// 在Controller之前执行
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 统计UV
            String ip = request.getRemoteHost();  // 获取IP
            dataService.recordUV(ip);        // 计入UV
    
            // 统计DAU
            User user = hostHolder.getUser();
            if (user != null) {
                dataService.recordDAU(user.getId());
            }
    
            return true;   // 请求继续向下执行
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    配置 DataInterceptor,拦截除静态资源之外的所有请求
    WebMvcConfig.java

        @Autowired
        private DataInterceptor dataInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
        	...
            registry.addInterceptor(dataInterceptor)
                    .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    关于数据存储的三道面试题,你会吗?
    【Unity3D】UI Toolkit容器
    贪心算法(算法竞赛、蓝桥杯)--排队接水问题
    【Net6】Net 5.0迁移到Net 6.0
    攻防世界m0_01
    2. 字符串
    【java零基础入门到就业】第三天:HelloWorld程序的常见问题和java环境变量的配置
    python趣味编程-太空入侵者游戏
    Ajax异步
    Java后台解决request请求体不能重复读取+解决XSS漏洞问题
  • 原文地址:https://blog.csdn.net/nice___amusin/article/details/126079598