271 lines
6.9 KiB
Markdown
271 lines
6.9 KiB
Markdown
# 网站数据统计
|
||
|
||
---
|
||
|
||
使用 Redis 高级数据类型:
|
||
|
||
<img src="https://gitee.com/veal98/images/raw/master/img/20210131104903.png" style="zoom: 33%;" />
|
||
|
||

|
||
|
||
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<String> 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<byte[]> 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;
|
||
}
|
||
}
|
||
```
|
||
|
||
别忘记在配置中添加此拦截器
|
||
|
||

|
||
|
||
## 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";
|
||
}
|
||
|
||
}
|
||
```
|
||
|
||

|
||
|
||
|
||
|
||
别忘记给管理员赋予权限:
|
||
|
||

|
||
|
||
## 前端
|
||
|
||
data.html
|
||
|
||
```html
|
||
<form method="post" th:action="@{/data/dau}">
|
||
<input type="date"name="start"
|
||
th:value="${#dates.format(dauStartDate, 'yyyy-MM-dd')}" />
|
||
<input type="date" required name="end"
|
||
th:value="${#dates.format(dauEndDate, 'yyyy-MM-dd')}" />
|
||
<button type="submit">开始统计</button>
|
||
</form>
|
||
统计结果 <span th:text="${dauResult}">
|
||
``` |