Redis进阶——BitMap用户签到HyperLogLog实现UV统计

news2025/2/23 23:49:29

目录

  • 用户签到
    • 实现签到功能
  • 签到统计
  • HyperLogLog实现UV统计
    • UV和PV的概述
    • 测试百万数据的统计

用户签到

BitMap功能演示
我们针对签到功能完全可以通过MySQL来完成,例如下面这张表
20240420-070809-7a.png

用户签到一次,就是一条记录,假如有1000W用户,平均每人每年签到10次,那这张表一年的数据量就有1亿条

那有没有方法能简化一点呢?我们可以使用二进制位来记录每个月的签到情况,签到记录为1,未签到记录为0

把每一个bit位对应当月的每一天,形成映射关系,用0和1标识业务状态,这种思路就成为位图(BitMap)。这样我们就能用极小的空间,来实现大量数据的表示

Redis中是利用String类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是2^32个bit位

BitMap的操作命令有:

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出现的位置

实现签到功能

需求:实现签到接口,将当前用户当天签到信息保存到Redis中

请求方式 Post
请求路径 /user/sign
请求参数 无
返回值 无

思路:我们可以把年和月作为BitMap的key,然后保存到一个BitMap中,每次签到就把对应位上的0变成1,只要是1就说明这一天已经签到了,反之则没有签到
由于BitMap底层是基于String数据结构,因此其操作也都封装在字符串相关操作中了
20240420-071557-oR.png

在UserController中编写对应的方法

@PostMapping("/sign")
public Result sign(){
    return userService.sign();
}

具体实现

@Override
public Result sign() {
    //1. 获取当前用户
    Long userId = UserHolder.getUser().getId();
    //2. 获取日期
    LocalDateTime now = LocalDateTime.now();
    //3. 拼接key
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    String key = USER_SIGN_KEY + userId + keySuffix;
    //4. 获取今天是当月第几天(1~31)
    int dayOfMonth = now.getDayOfMonth();
    //5. 写入Redis  BITSET key offset 1
    stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
    return Result.ok();
}

通过直接使用登录用户信息,借助Redis的BitMap进行简单的签到记录。

签到统计

如何获取本月到今天为止的所有签到数据?

BITFIELD key GET u[dayOfMonth] 0

如何从后往前遍历每个bit位,获取连续签到天数
连续签到天数,就是从末尾往前数,看有多少个1
简单的位运算算法

int count = 0;
while(true) {
    if((num & 1) == 0)
        break;
    else
        count++;
    num >>>= 1;
}
return count;

需求:实现下面接口,统计当前用户截止当前时间在本月的连续签到天数

请求方式 GET
请求路径 /user/sign/count
请求参数 无
返回值 连续签到天数

代码实现:
在UserController中创建对应的方法

@GetMapping("/sign/count")
public Result signCount(){
    return userService.signCount();
}

在UserServiceImpl中实现方法

@Override
public Result signCount() {
    //1. 获取当前用户
    Long userId = UserHolder.getUser().getId();
    //2. 获取日期
    LocalDateTime now = LocalDateTime.now();
    //3. 拼接key
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    String key = USER_SIGN_KEY + userId + keySuffix;
    //4. 获取今天是当月第几天(1~31)
    int dayOfMonth = now.getDayOfMonth();
    //5. 获取截止至今日的签到记录  BITFIELD key GET uDay 0
    List<Long> result = stringRedisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
            .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
    if (result == null || result.isEmpty()) {
        return Result.ok(0);
    }
    //6. 循环遍历
    int count = 0;
    Long num = result.get(0);
    while (true) {
        if ((num & 1) == 0) {
            break;
        } else
            count++;
        //数字右移,抛弃最后一位
        num >>>= 1;
    }
    return Result.ok(count);
}

上述通过Redis的BitMap数据结构,加上简单的与运算,计算出来到今天为止连续签到的次数,计算的逻辑是使用计数器统计从末尾往前数,看有多少个1,判断1的依据是使用与运算。

HyperLogLog实现UV统计

UV和PV的概述

UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。

通常来说PV会比UV大很多,所以衡量同一个网站的访问量,我们需要综合考虑很多因素。
UV统计在服务端做会很麻烦,因为要判断该用户是否已经统计过了,需要将统计过的信息保存,但是如果每个访问的用户都保存到Redis中,那么数据库会非常恐怖,那么该如何处理呢?

HyperLogLog(HLL)是从Loglog算法派生的概率算法,用户确定非常大的集合基数,而不需要存储其所有值,算法相关原理可以参考下面这篇文章:https://juejin.cn/post/6844903785744056333#heading-0

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

PFADD key element [element...]
summary: Adds the specified elements to the specified HyperLogLog

PFCOUNT key [key ...]
Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s).

PFMERGE destkey sourcekey [sourcekey ...]
lnternal commands for debugging HyperLogLog values

测试百万数据的统计

使用单元测试,向HyperLogLog中添加100万条数据,看看内存占用是否真的那么低,以及统计误差如何

@Test
public void testHyperLogLog() {
    String[] users = new String[1000];
    int j = 0;
    for (int i = 0; i < 1000000; i++) {
        j = i % 1000;
        users[j] = "user_" + i;
        if (j == 999) {
            stringRedisTemplate.opsForHyperLogLog().add("HLL", users);
        }
    }
    Long count = stringRedisTemplate.opsForHyperLogLog().size("HLL");
    System.out.println("count = " + count);
}

执行结果:
20240420-072658-02.png

插入100W条数据,得到的count为997593,误差率为0.002407%
去Redis图形化界面中查看占用情况为:12.3K字节

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1609384.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RCE漏洞及其绕过——[SWPUCTF 2021 新生赛]easyrce、caidao、babyrce

目录 什么是Shell 1、Shell简介 2、印刷约定 一、什么是RCE 漏洞产生条件&#xff1a; 漏洞检测&#xff1a; 1.远程命令执行 system()函数&#xff1a; passthru()函数&#xff1a; exec()函数&#xff1a; 无回显 shell_exec()函数&#xff1a; 2.远程代码执行 e…

我们一起看看《看漫画学C++》中如何讲解对象的动态创建与销毁

《看漫画学C》这本书中会用图文的方式生动地解释对象的动态创建与销毁。在C中&#xff0c;动态创建对象是通过new运算符来实现的&#xff0c;而销毁对象则是通过delete运算符来完成的。这种方式可以让程序在需要时分配内存给对象&#xff0c;并在对象不再需要时释放内存&#x…

「Word 论文排版」插入分节符导致word转PDF后出现空白页

问题 word转PDF后出现空白页 解决 但是此方法会让页面页脚标记出错 TODO 如下图所示 在论文目录后有一个分节符&#xff0c;转成PDF之后就多了一个空白页 文件-打印-页面设置-选中封面那一页-版式-从偶数页开始 再导出空白页就没了

Nginx莫名奇妙返回了404

描述 nginx作为反向代理&#xff0c;代理python的服务&#xff0c;但是通过代理访问服务的时候&#xff0c;报了404的错误。 难受的是客户现场没有查看日志的权限&#xff0c;只有查看配置文件的权限&#xff0c;我们检测了几遍配置文件也没有找到问题&#xff0c;哎~ 问题引…

34. 【Android教程】菜单:Menu

作为 Android 用户&#xff0c;你一定见过类似这样的页面&#xff1a; 它就是我们今天的主角——菜单&#xff0c;它的使用场景和作用不用多说&#xff0c;几乎每个 App 都会用到它&#xff0c;今天我们就一起来看看 Android 提供的几种菜单类型及用法。 1. 菜单的几种类型 根…

[Algorithm][滑动窗口][无重复字符的最长字串][最大连续的一个数 Ⅲ][将x减到0的最小操作数]详细讲解

目录 1.无重复字符的最长字串1.题目链接2.算法原理详解3.代码实现 2.最大连续的一个数 Ⅲ1.题目链接2.算法原理详解3.代码实现 3.将x减到0的最小操作数1.题目链接2.算法原理详解3.代码实现 1.无重复字符的最长字串 1.题目链接 无重复字符的最长字串 2.算法原理详解 研究的对…

美化博客文章(持续更新)

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;游戏实现&#xff1a;贪吃蛇​​​​​​ &#x1f337;追光的人&#xff0c;终会万丈光芒 前言&#xff1a; 该文提供我的一些文章设计的一些方法 目录 1.应用超链接 1.应用超链接

URL GET +号后台接收成空格

问题&#xff1a;参数spdmwhbs001 其中包含URL特殊符号 如果用GET请求方式不做任何不处理那么浏览器自动将转为%20 请求链接为 details?spdmwhbs%20001&limitKcysType1 后台接收到的参数为 whbs 001 &#xff0c;自动将号转成空格了。 尝试解决&#xff08;失败&#…

使用FastDDS编译IDL文件

1.安装FastDDS环境 Ubuntu22.04 1.1安装依赖的软件 sudo apt-get update //基础工具安装 sudo apt install cmake g python3-pip wget git //Asio 是一个用于网络和低级 I/O 编程的跨平台C库&#xff0c;它提供了一致的 异步模型。 TinyXML2是一个简单&#xff0c;小巧&…

400电话如何对接配置SIP

400电话对接配置SIP的基本步骤 要配置400电话对接SIP&#xff0c;通常需要遵循以下基本步骤&#xff1a; 注册和认证&#xff1a;首先需要在相应的云通信平台上注册账号&#xff0c;并进行企业实名认证。 开通语音服务&#xff1a;在通过认证后&#xff0c;需要开通语音服务&…

C# winform s7.net expected 22 bytes.”

S7.Net.PlcException:“Received 12 bytes: 32-02-00-00-00-00-00-00-00-00-81-04, expected 22 bytes.” 原因是博图的连接机制未勾选

Java编程题 | 数组元素交换

大家可以关注一下专栏&#xff0c;方便大家需要的时候直接查找&#xff0c;专栏将持续更新~ 题目描述 编写一个Java程序&#xff0c;输入一个整数数组&#xff0c;将最大的元素与第一个元素交换&#xff0c;最小的元素与最后一个元素交换&#xff0c;然后输出修改后的数组…

数据结构从入门到实战——顺序表的应用

目录 一、基于动态顺序表实现通讯录 二、代码实现 2.1 通讯录的初始化 2.2 通讯录的销毁 2.3 通讯录的展示 2.4 通讯录添加联系人信息 2.5 通讯录删除联系人信息 2.6 通讯录修改联系人信息 2.7 通讯录的查找联系人信息 2.8 将通讯录中联系人信息保存到文件中 2.9…

【Qt】设置QT标准对话框为中文字体

设置QT标准对话框为中文字体 一、问题二、解决方法1、找到Qt内置的翻译文件 qt_zh_CN.qm2、在代码中加载该文件 一、问题 在Qt中我们使用的标准对话框都是英文&#xff0c;例如下面的 字体选择对话框&#xff0c;但是实际中我们需要构建的是中文对话框。 所以我们需要使用Qt官…

【redis】hash和list常用命令

hash类型 Redis自身已经是键值对结构了。Redis自身的键值对就是通过哈希的方式来组织的。 把key这一层组织完成之后,到了value这一层。value的其中一种类型还可以再是哈希。哈希类型中的映射关系通常称为field-value,用于区分Redis整体的键值对(key-value)。注意这里的 value…

为什么创业总是小部分人能成功?

每年都有大批量应届生等待就业&#xff0c;同时&#xff0c;也有一大批大龄中青人正在面临失业。在这种焦虑&#xff0c;内卷的氛围下&#xff0c;就会激发一波又一波的人走上创业的道路。想靠创业改变命运。辞职之后&#xff0c;第一时间能想到的创业方式要么就是就是 开个奶茶…

阿里云ECS服务器安装docker

首先查看阿里云ECS的服务器的版本 cat /etc/redhat-release如果是Alibaba Cloud Linux release 3,请执行以下命令 添加docker-ce的dnf源。 sudo dnf config-manager --add-repohttps://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo安装Alibaba Cloud Linux 3专…

【若依前后端分离】仪表盘绘制

示例&#xff1a; 代码&#xff1a; InstrumentPanel.vue组件 <template><div><!-- 在这里放置你的图表组件 --><div ref"echarts" style"width: 100%; height: 400px;"></div></div> </template><script&g…

如何将你的iOS应用成功上架App Store(图文详解)

上架基本需求资料 1、苹果开发者账号&#xff08;如还没账号先申请- 苹果开发者账号申请教程&#xff09; 2、开发好的APP 通过本篇教程&#xff0c;可以学习到ios证书申请和打包ipa上传到appstoreconnect.apple.com进行TestFlight测试然后提交审核的完整流程&#xff01; …

基于javaspringboot实现的垃圾分类网站

开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven…