文章目录
- 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>