用redis做用户访问数据统计HyperLogLog及Bitmap高级数据类型


使用redis中的HyperLogLog来做活跃量统计

方法步骤:

  1. 因为要用的Redis,所以要确定Key的命名。
  2. 编写用户统计的 增 查(当天or间隔)方法
  3. 使用拦截器来 调 增方法(我们的页面被访问后,就需要对用户记录,进而改变Redis中的统计数据)
  4. 写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两个状态

  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>

  1. 查看某天是否打卡
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>
  1. 统计操作
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

  1. 因为单线程处理一个访问流程,所以使用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");
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值