# 网站数据统计 --- 使用 Redis 高级数据类型: ![](https://gitee.com/veal98/images/raw/master/img/20210131112124.png) DAU 只统计登录用户,UV 兼并访客和登录用户 DAU:将用户 ID 作为 bitmap 的 key 存入,若该用户当天访问过,则置 value 为 1。一天用一个 bitmap 计算区间内的 DAU 的时候, 使用 or 运算,就是说这么多天只要这个用户登陆过一次,就算活跃用户 RedisKeyUtil: ```java /** * 单日 UV * @param date * @return */ public static String getUVKey(String date) { return PREFIX_UV + SPLIT + date; } /** * 区间 UV * @param startDate * @param endDate * @return */ public static String getUVKey(String startDate, String endDate) { return PREFIX_UV + SPLIT + startDate + SPLIT + endDate; } /** * 单日 DAU * @param date * @return */ public static String getDAUKey(String date) { return PREFIX_DAU + SPLIT + date; } /** * 区间 DAU * @param startDate * @param endDate * @return */ public static String getDAUKey(String startDate, String endDate) { return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate; } ``` ## Service ```java /** * 网站数据统计(UV / DAU) */ @Service public class DataService { @Autowired private RedisTemplate redisTemplate; private SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); /** * 将指定的 IP 计入当天的 UV * @param ip */ public void recordUV(String ip) { String redisKey = RedisKeyUtil.getUVKey(df.format(new Date())); redisTemplate.opsForHyperLogLog().add(redisKey, ip); } /** * 统计指定日期范围内的 UV * @param start * @param end * @return */ public long calculateUV(Date start, Date end) { if (start == null || end == null) { throw new IllegalArgumentException("参数不能为空"); } // 整理该日期范围内的 key List keyList = new ArrayList<>(); Calendar calendar = Calendar.getInstance(); calendar.setTime(start); while (!calendar.getTime().after(end)) { String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime())); keyList.add(key); calendar.add(Calendar.DATE, 1); // 加1天 } // 合并这些天的 UV String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end)); redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray()); // 返回统计结果 return redisTemplate.opsForHyperLogLog().size(redisKey); } /** * 将指定的 IP 计入当天的 DAU * @param userId */ public void recordDAU(int userId) { String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date())); redisTemplate.opsForValue().setBit(redisKey, userId, true); } /** * 统计指定日期范围内的 DAU * @param start * @param end * @return */ public long calculateDAU(Date start, Date end) { if (start == null || end == null) { throw new IllegalArgumentException("参数不能为空"); } // 整理该日期范围内的 key List keyList = new ArrayList<>(); 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); // 加1天 } // 进行 or 运算 return (long) redisTemplate.execute(new RedisCallback() { @Override public Object doInRedis(RedisConnection redisConnection) throws DataAccessException { String redisKey = RedisKeyUtil.getDAUKey(df.format(start), df.format(end)); redisConnection.bitOp(RedisStringCommands.BitOperation.OR, redisKey.getBytes(), keyList.toArray(new byte[0][0])); return redisConnection.bitCount(redisKey.getBytes()); } }); } } ``` ## 拦截器 在所有请求执行之前查询 uv 和 dau ```java @Component public class DataInterceptor implements HandlerInterceptor { @Autowired private DataService dataService; @Autowired private HostHolder hostHolder; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 统计 UV String ip = request.getRemoteHost(); dataService.recordUV(ip); // 统计 DAU User user = hostHolder.getUser(); if (user != null) { dataService.recordDAU(user.getId()); } return true; } } ``` 别忘记在配置中添加此拦截器 ![](https://gitee.com/veal98/images/raw/master/img/20210131130906.png) ## Controller ```java /** * 网站数据 */ @Controller public class DataController { @Autowired private DataService dataService; /** * 进入统计界面 * @return */ @RequestMapping(value = "/data", method = {RequestMethod.GET, RequestMethod.POST}) public String getDataPage() { return "/site/admin/data"; } /** * 统计网站 uv * @param start * @param end * @return */ @PostMapping("/data/uv") 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.addAttribute("uvEndDate", end); return "forward:/data"; } /** * 统计网站 DAU * @param start * @param end * @return */ @PostMapping("/data/dau") 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"; } } ``` ![](https://gitee.com/veal98/images/raw/master/img/20210131121136.png) 别忘记给管理员赋予权限: ![](https://gitee.com/veal98/images/raw/master/img/20210131130739.png) ## 前端 data.html ```html
统计结果 ```