(黑马点评)八、实现签到统计和uv统计

8.1 签到统计系列功能

8.1.1 认识BitMap结构

BitMap是Redis基于String实现的一种高效的二进制数位的数据结构。因此一个BItMap的最大上线为512M,转为bit位可表示 2^32位

常见命令

SETBIT:向指定位置(offset)存入一个0或1

GETBIT :获取指定位置(offset)的bit值

BITCOUNT :统计BitMap中值为1的bit位的数量

BITFIELD :操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值

BITFIELD_RO :获取BitMap中bit数组,并以十进制形式返回

BITOP :将多个BitMap的结果做位运算(与 、或、异或)

BITPOS :查找bit数组中指定范围内第一个0或1出现的位置

8.1.2 为什么要使用BitMap结构进行统计

传统的数据库表统计方式:十分耗费存储内存

采用二进制位的BitMap统计方式:高效、内存占用少、

key用来指定统计者的信息:姓名、编号、年月

vlaue采用二进制的0 | 1 串标识签到状况 ,0表示为未签到 1 表示为已签到

8.1.3 使用BitMap实现用户签到统计

按月来统计用户签到信息,签到记录为1,未签到则记录为0.

签到功能实现流程

1. 获取当前用户信息

2. 获取当前日期信息

3. 拼接业务key

4. 使用setBit(key,offset,value)实现签到打卡

java 复制代码
    /**
     * 签到统计
     * @return
     */
    @PostMapping("/sign")
    public Result sign(){
        return userService.sign();
    }


/**
     * 签到功能
     * @return
     */
    @Override
    public Result sign() {
        //1. 获取当前的登陆用户
        UserDTO user = UserHolder.getUser();
        //2. 获取当前日期
        LocalDateTime now = LocalDateTime.now();
        //3. 拼接业务的key
        String keySuffix = now.format(DateTimeFormatter.ofPattern("yyyy/MM"));
        String key =
                USER_SIGN_KEY +
                        user.getNickName() +
                        '_' + user.getId() +
                        '_' + keySuffix;
        //4. 获取签到日期
        int day = now.getDayOfMonth();
        //5. 写入Redis SETBIT key offset ture/false
        stringRedisTemplate.opsForValue().setBit(key, day-1, true);


        return Result.ok();
    }

直接发送签到请求

查看Redis写入的签到卡

8.1.4 使用BitMap实现用户连续签到天数统计

实现统计连续签到天数的流程

1. 获取当前登录用户信息

2. 获取日期信息

3. 拼接业务key

4. 使用 stringRedisTemplate.opsForValue()
.bitField(key,
BitFieldSubCommands
.create()
.get(BitFieldSubCommands
.BitFieldType
.unsigned(dayOfMonth))
.valueAt(0)); 获取统计信息【10进制】

5. 判空

6. 循环右移 + 与1 运算 得出连续签到天数

7. 返回结果

java 复制代码
/**
     * 连续签到天数统计
     * @return
     */
    @GetMapping("/sign/count")
    public Result countSign(){
        return userService.countSign();
    }


/**
     * 连续签到天数统计
     * @return
     */
    @Override
    public Result countSign() {
        //1. 获取当前登录用户
        UserDTO user = UserHolder.getUser();
        //2. 获取当前日期
        LocalDateTime now = LocalDateTime.now();
        int dayOfMonth = now.getDayOfMonth();
        //3.拼接业务的key
        String keySuffix = now.format(DateTimeFormatter.ofPattern("yyyy/MM"));
        String key = USER_SIGN_KEY +
                    user.getNickName() +
                    '_' + user.getId() +
                    '_' + keySuffix;
        //4. 获取当前用户签到数据(返回十进制数)
        List<Long> signNum = stringRedisTemplate.opsForValue()
                .bitField(key,
                        BitFieldSubCommands
                                .create()
                                .get(BitFieldSubCommands
                                        .BitFieldType
                                        .unsigned(dayOfMonth))
                                .valueAt(0));
        if(signNum == null || signNum.isEmpty()){
            return Result.ok(0);
        }
        Long num = signNum.get(0);
        if(num == null || num == 0){
            return Result.ok(0);
        }

        //5. 循环右移 + 1与运算 得出连续签到天数
        int dayCount = 0;
        while(true) {
            if(((num & 1) == 0)){
                break;
            }else{
                dayCount ++;
            }
            num = num >> 1;
        }
        return Result.ok(dayCount);
    }

发送连续签到统计请求

8.2 百万级UV统计功能实现

8.2.1 百万级UV统计的实现策略------HyperLogLog

什么是UV统计

UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。

什么是PV统计

PV:全称Page View,也叫页面访问量或点击量 ,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量

传统方式实现UV统计面对的难题

UV统计在服务端做会比较麻烦,因为要判断该用户是否已经统计过了,需要将统计过的用户信息保存。但是如果每个访问的用户都保存到****Redis中,数据量会非常恐怖。
高效的解决措施------使用HyperLogLog

Hyperloglog(HLL)是从Loglog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。

Redis中的HLL是基于string结构实现的,单个****HLL的内存永远小于16kb ,内存占用低的令人发指!作为代价,其测量结果是概率性的,有小于****0.81 %的误差。不过对于UV统计来说,这完全可以忽略。
HyperLogLog的原理

哈希处理

对每个要加入的元素进行哈希处理。这一步骤将元素映射为一个固定长度的二进制串。

划分与索引------分桶

在 Redis 的实现中,使用 64bit 的 Hash 函数,其中 14bit 用于桶索引(将原始集合分成多个子集),剩下的 50bit 用于计算前导零的个数。这意味着 Redis 中的 HyperLogLog 使用16384个桶,每个桶可以表达的最大数字是:2^5+2^4+...+1 = 63

估计基数------桶估计值公式

通过使用补偿和线性计数的技术,将最大前导零位转换为基数估计值 。具体的计算方法可以使用查表或其他数学模型来实现。在 Redis 的 HyperLogLog 实现中,有一个基于经验值的参数 alpha_m 用于**调整估计值,**以确保在大多数情况下都能提供较为准确的估计。

误差修正------偏差修正因子

由于 HyperLogLog 是一种概率性算法,其估计结果会存在一定的误差。Redis 通过应用修正公式来纠正这些估计误差,从而提供更加准确的基数估计。

合并与查询 HyperLogLog 支持多个数据集的并集操作,可以将多个 Key 的 UV 进行去重合并,且合并的复杂度是 O(1)。同时,获取一个或多个 HyperLogLog 结构的基数估计值也非常高效,查询的复杂度同样是 O(1)。

更多请看: HyperLogLog 算法的原理讲解以及 Redis 是如何应用它的聪明的你可能会马上想到,用 HashMap 这种数 - 掘金 (juejin.cn)

8.2.2 使用HyperLogLog实现百万级UV统计测试

我们向Redis插入100万条数据,用来模拟超大UV访问量的统计,并计算使用HyperLogLog存储需要消耗的内存大小以及统计误差,从而向大家展示Hyper在统计方面的优秀性能

java 复制代码
/**
     * 向Hyperloglog插入100万条数据进行测试,查看内存占用情况
     */
    @Test
    void testHyperLogLog() {
        // 开始循环
        for(int i=0;i<1000;i++){
            String[] values = new String[1000];
            for(int j=0;j<1000;j++){
                values[j] = "user_" + i + "_" + j;
            }
            stringRedisTemplate.opsForHyperLogLog().add("hl2",values);
        }

        // 统计数量
        Long count = stringRedisTemplate.opsForHyperLogLog().size("hl2");
        System.out.println("count = " + count);

    }
相关推荐
miss writer7 分钟前
Redis分布式锁释放锁是否必须用lua脚本?
redis·分布式·lua
Mr.131 小时前
数据库的三范式是什么?
数据库
Cachel wood1 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
Code哈哈笑1 小时前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
Python之栈1 小时前
【无标题】
数据库·python·mysql
风_流沙1 小时前
java 对ElasticSearch数据库操作封装工具类(对你是否适用嘞)
java·数据库·elasticsearch
亽仒凣凣1 小时前
Windows安装Redis图文教程
数据库·windows·redis
亦世凡华、2 小时前
MySQL--》如何在MySQL中打造高效优化索引
数据库·经验分享·mysql·索引·性能分析
YashanDB2 小时前
【YashanDB知识库】Mybatis-Plus调用YashanDB怎么设置分页
数据库·yashandb·崖山数据库
ProtonBase2 小时前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构