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

2024-04-20 10:44

本文主要是介绍Redis进阶——BitMap用户签到HyperLogLog实现UV统计,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 用户签到
    • 实现签到功能
  • 签到统计
  • 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. 拼接keyString 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 1stringRedisTemplate.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;elsecount++;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. 拼接keyString 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 0List<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;} elsecount++;//数字右移,抛弃最后一位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 HyperLogLogPFCOUNT 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字节

这篇关于Redis进阶——BitMap用户签到HyperLogLog实现UV统计的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/920042

相关文章

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

Python实现字典转字符串的五种方法

《Python实现字典转字符串的五种方法》本文介绍了在Python中如何将字典数据结构转换为字符串格式的多种方法,首先可以通过内置的str()函数进行简单转换;其次利用ison.dumps()函数能够... 目录1、使用json模块的dumps方法:2、使用str方法:3、使用循环和字符串拼接:4、使用字符

Redis 基本数据类型和使用详解

《Redis基本数据类型和使用详解》String是Redis最基本的数据类型,一个键对应一个值,它的功能十分强大,可以存储字符串、整数、浮点数等多种数据格式,本文给大家介绍Redis基本数据类型和... 目录一、Redis 入门介绍二、Redis 的五大基本数据类型2.1 String 类型2.2 Hash

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三

Redis中Set结构使用过程与原理说明

《Redis中Set结构使用过程与原理说明》本文解析了RedisSet数据结构,涵盖其基本操作(如添加、查找)、集合运算(交并差)、底层实现(intset与hashtable自动切换机制)、典型应用场... 目录开篇:从购物车到Redis Set一、Redis Set的基本操作1.1 编程常用命令1.2 集

Linux下利用select实现串口数据读取过程

《Linux下利用select实现串口数据读取过程》文章介绍Linux中使用select、poll或epoll实现串口数据读取,通过I/O多路复用机制在数据到达时触发读取,避免持续轮询,示例代码展示设... 目录示例代码(使用select实现)代码解释总结在 linux 系统里,我们可以借助 select、