牛客网后端项目实战(四十一):网站统计数据

文章目录

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

Rediskey

RedisKeyUtil.java

    // 单日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;
    }

Service

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

UV(Unique Visitor)

1、将指定的IP计入UV

@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)private SimpleDateFormat df = new SimpleDateFormat(“yyyyMMdd”) 先指定日期格式

2、统计:统计指定日期范围内的UV

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

DAU(Dail Active User)

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

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

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的个数
            }
        });
    }
}

Controller

@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、"/data" 路径打开网站统计页面
2、path = “/data/uv” 统计UV。
1)@DateTimeFormat(pattern = “yyyy-MM-dd”) :Date start,是对日期参数的处理。
SpringMVC中Controller中方法参数为Date类型想要限定请求传入时间格式时,可以通过@DateTimeFormat来指定,但请求传入参数与指定格式不符时,会返回400错误。
Spring中时间格式注解@DateTimeFormat

2)return “forward:/data”:forward请求转发。声明该方法只能处理一半,还需要另外一个方法接着处理,请求转发到了 “/data” 路径,由于还是同一个请求,所以"/data"路径也要支持 RequestMethod.POST 请求
https://blog.csdn.net/u011676300/article/details/79657933
http://www.51gjie.com/javaweb/956.html

拦截器

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

@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;   // 请求继续向下执行
    }
}

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

    @Autowired
    private DataInterceptor dataInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    	...
        registry.addInterceptor(dataInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }

模板

		<!-- 内容 -->
		<div class="main">
			<!-- 网站UV -->
			<div class="container pl-5 pr-5 pt-3 pb-3 mt-3">
				<h6 class="mt-3"><b class="square"></b> 网站 UV</h6>
				<form class="form-inline mt-3" method="post" th:action="@{/data/uv}"> // 请求路径
					<input type="date" class="form-control" required name="start" th:value="${#dates.format(uvStartDate,'yyyy-MM-dd')}"/> // name="start" th:value 值是格式化了的日期类 uvStartDate
					<input type="date" class="form-control ml-3" required name="end" th:value="${#dates.format(uvEndDate,'yyyy-MM-dd')}"/>
					<button type="submit" class="btn btn-primary ml-3">开始统计</button>
				</form>
				<ul class="list-group mt-3 mb-3">
					<li class="list-group-item d-flex justify-content-between align-items-center">
						统计结果
						<span class="badge badge-primary badge-danger font-size-14" th:text="${uvResult}">0</span>
					</li>
				</ul>
			</div>

<input type=“date” > :input时间表单默认样式
input时间表单默认样式修改(input[type=“date”])

活跃用户的模板类似

			<!-- 活跃用户 -->
			<div class="container pl-5 pr-5 pt-3 pb-3 mt-4">
				<h6 class="mt-3"><b class="square"></b> 活跃用户</h6>
				<form class="form-inline mt-3" method="post" th:action="@{/data/dau}">
					<input type="date" class="form-control" required name="start" th:value="${#dates.format(dauStartDate,'yyyy-MM-dd')}"/>
					<input type="date" class="form-control ml-3" required name="end" th:value="${#dates.format(dauEndDate,'yyyy-MM-dd')}"/>
					<button type="submit" class="btn btn-primary ml-3">开始统计</button>
				</form>
				<ul class="list-group mt-3 mb-3">
					<li class="list-group-item d-flex justify-content-between align-items-center">
						统计结果
						<span class="badge badge-primary badge-danger font-size-14" th:text="${dauResult}">0</span>
					</li>
				</ul>
			</div>				
		</div>
上一篇:Storm实时计算网站的UV


下一篇:紫外线消毒盒开发方案