• 用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;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    编写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);
        }
    
        
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    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
    • 2
    • 3
    • 4
    • 5
    1. 统计操作
    127.0.0.1:6379> bitcount attendence #统计出勤天数
    (integer) 5
    
    • 1
    • 2

    确定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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    编写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());
            }
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    编写拦截器

    写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();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    写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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    写数据统计拦截器组件

    @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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    配置类中注册拦截器

    为了使用拦截器,需要在配置类中注册我们写的几个拦截器组件。

    注: 注册顺序 = 执行顺序

    @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");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ptorRegistry registry) {

        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    
        registry.addInterceptor(dataInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    }

    
    
    
    
    
    
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    最新最管用的nvm安装步骤及nvm报443超时解决方案
    设计模式之【工厂模式、适配器模式】
    Linux学习(7)——开机、重启和用户管理
    面试常问:HTTPS与HTTP的区别
    【IMX6ULL笔记】-- 从驱动到应用(基于Qt)- LED
    【洛谷 P8655】[蓝桥杯 2017 国 B] 发现环 题解(邻接表+并查集+路径压缩)
    十六、Webpack常见的插件和模式
    【Javascript保姆级教程】显示类型转换和隐式类型转换
    .NET 高级开发人员面试常见问题及解答
    Flink系列之Flink中Broadcast和Counter整理和实战
  • 原文地址:https://blog.csdn.net/qq_44220795/article/details/125470379