文章目录
使用redis中的HyperLogLog来做活跃量统计
方法步骤:
- 因为要用的Redis,所以要确定Key的命名。
- 编写用户统计的 增 查(当天or间隔)方法
- 使用拦截器来 调 增方法(我们的页面被访问后,就需要对用户记录,进而改变Redis中的统计数据)
- 写Controller 实现 查 方法的调用
UV:页面访问量
DAU:页面活跃用户
两者区别:UV用 ip来记录,DAU用User来记录。即用户就算没登录页面,但打开了页面,访问量+1。只有用户登录了,代表活跃。
HyperLogLog做日访问量UV统计
HyperLogLog简介
基数
A{1 2 3 4 5}
B{4 5 6 7 8}
基数:8(统计去除重复元素的个数)
优点
占用内存是固定的,只需要占用12kb的内存!但是有0.81%的错误率,用于统计网页访问人数是可以接受度的。
确定Key
public class RedisKeyUtil {
private static final String PREFIX_UV = "uv";
private static final String PREFIX_DAU = "dau";
private static final String SPLIT = ":";
// 单日UV uv:20200812 代表2020年8月12日
public static String getUVKey(String date) {
return PREFIX_UV + SPLIT + date;
}
// 区间UV uv:20201001:20201010 代表2020年10月1日至2020年10月10日
public static String getUVKey(String startDate, String endDate) {
return PREFIX_UV + SPLIT + startDate + SPLIT + endDate;
}
}
编写Service 用户访问量的增 查
@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);
}
// 统计指定日期范围内的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()));
keyList.add(key);
calendar.add(Calendar.DATE, 1);
}
// 合并这些数据
String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));
redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());
// 返回统计的结果
return redisTemplate.opsForHyperLogLog().size(redisKey);
}
}
Bitmap做日活跃用户DAU统计
Bitmap简介
位存储
统计用户信息是否活跃,是否经常登录,是否打卡,两种状态的。Bitmap位图都是操作二进制来进行记录,只有0和1两个状态
- 查看用户一个星期的打卡情况
语法:setbit key offset value 且offset 和value必须为数字,value必须为数字0或1
#记录attendence中周日(0代表周日)未出勤(0代表未出勤)
127.0.0.1:6379> setbit attendence 0 0
(integer) 0
#记录attendence中周日(1代表周一)出勤(1代表未出勤)
127.0.0.1:6379> setbit attendence 1 1
(integer) 0
#记录attendence中周日(2代表周二)出勤(1代表未出勤)
127.0.0.1:6379> setbit attendence 2 1
(integer) 0
#记录attendence中周日(3代表周三)出勤(1代表未出勤)
127.0.0.1:6379> setbit attendence 3 1
(integer) 0
127.0.0.1:6379> setbit attendence 4 1
(integer) 0
127.0.0.1:6379> setbit attendence 5 1
(integer) 0
#记录attendence中周六(6代表周六)未出勤(0代表未出勤)
127.0.0.1:6379> setbit attendence 6 0
(integer) 0
127.0.0.1:6379>
- 查看某天是否打卡
127.0.0.1:6379> getbit attendence 0#查看周日是否出勤,未出勤则返回0
(integer) 0
127.0.0.1:6379> getbit attendence 3 #查看周四是否出勤,出勤则返回1
(integer) 1
127.0.0.1:6379>
- 统计操作
127.0.0.1:6379> bitcount attendence #统计出勤天数
(integer) 5
确定Key
// 单日活跃用户
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 用户访问量的增 查
setBit(key,userid,ture)
= 在key上的第userid那一位上置为1
代表:第key天,user为的登录过
查询月活跃用户,对30天的value做 或运算,因为同一个用户登录10天 其月活跃用户也计算为1个。
// 将指定用户计入DAU
public void recordDAU(int userId) {
String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date()));
redisTemplate.opsForValue().setBit(redisKey, userId, true);
}
// 统计指定日期范围内的DAU
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);
}
// 进行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,
redisKey.getBytes(), keyList.toArray(new byte[0][0]));
return connection.bitCount(redisKey.getBytes());
}
});
}
编写拦截器
写HostHolder
- 因为单线程处理一个访问流程,所以使用ThreadLocal来 存储用户对象
/**
* 持有用户信息,用于代替session对象.
*/
@Component
public class HostHolder {
private ThreadLocal<User> users = new ThreadLocal<>();
public void setUser(User user) {
users.set(user);
}
public User getUser() {
return users.get();
}
public void clear() {
users.remove();
}
}
写HostHolder拦截器组件
这里主要做以下几点操作:
- preHandle中(进入Controller前),通过cookie获取用户User实体,同时存入HostHolder
- afterCompletion中(完成Controller操作后),hostHolder.clear(),清除ThreadLocal中的User对象。
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从cookie中获取凭证
String ticket = CookieUtil.getValue(request, "ticket");
if (ticket != null) {
// 查询凭证
LoginTicket loginTicket = userService.findLoginTicket(ticket);
// 检查凭证是否有效
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
// 根据凭证查询用户
User user = userService.findUserById(loginTicket.getUserId());
// 在本次请求中持有用户
hostHolder.setUser(user);
// 构建用户认证的结果,并存入SecurityContext,以便于Security进行授权.
Authentication authentication = new UsernamePasswordAuthenticationToken(
user, user.getPassword(), userService.getAuthorities(user.getId()));
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user = hostHolder.getUser();
if (user != null && modelAndView != null) {
modelAndView.addObject("loginUser", user);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
hostHolder.clear();
SecurityContextHolder.clearContext();
}
}
写数据统计拦截器组件
@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;
}
}
配置类中注册拦截器
为了使用拦截器,需要在配置类中注册我们写的几个拦截器组件。
注: 注册顺序 = 执行顺序
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Autowired
private DataInterceptor dataInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
registry.addInterceptor(dataInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}