存储过程 not supported yet_让我们来看看+Redis如何存储和计算一亿用户的活跃度

1

前段时间,在网上看到一道面试题:

如何用redis存储统计1亿用户一年的登陆情况,并快速检索任意时间窗口内的活跃用户数量。

觉得很有意思,就仔细想了下 。并做了一系列实验,自己模拟了下 。还是有点收获的,现整理下来。和大家一起分享。

Redis是一个内存数据库,采用单线程和事件驱动的机制来处理网络请求。实际生产的QPS和TPS单台都能达到3,4W,读写性能非常棒。用来存储一些对核心业务弱影响的用户状态信息还是非常不错的。

对于这题,有2个重要的点需要考虑:

1.如何用合适的数据类型来存储1亿用户的数据,用普通的字符串来存储肯定不行。经过查看一个最简单的kv(key为aaa,value为1)的内存占用,发现为48byte。

e8e9298ce0bb240afdb8a09c8c1850ff.png

假设每个用户每天登陆需要占据1对KV的话,那一亿就是(48*100000000)/1024/1024/1024=4.47G。这还是一天的量。

2.如何满足搜索,redis是一个键值对的内存结构,只能根据key来进行定位value值,无法做到像elastic search那样对文档进行倒排索引快速全文检索。

redis其实有这种数据结构的,可以以很少的空间来存储大量的信息。

2

在redis 2.2.0版本之后,新增了一个位图数据,其实它不是一种数据结构。实际上它就是一个一个字符串结构,只不过value是一个二进制数据,每一位只能是0或者1。redis单独对bitmap提供了一套命令。可以对任意一位进行设置和读取。

bitmap的核心命令:

SETBIT

语法:SETBIT key offset value

例如:

setbit abc 5 1 ----> 00001

setbit abc 2 1 ----> 00101

GETBIT

语法:GETBIT key offset

例如:

getbit abc 5 ----> 1

getbit abc 1 ----> 0

bitmap的其他命令还有bitcount,bitcount,bitpos,bitop等命令。都是对位的操作。

因为bitmap的每一位只占据1bit的空间 ,所以利用这个特性我们可以把每一天作为key,value为1亿用户的活跃度状态。假设一个用户一天内只要登录了一次就算活跃。活跃我们就记为1,不活跃我们就记为0。把用户Id作为偏移量(offset)。这样我们一个key就可以存储1亿用户的活跃状态。

8b40a841dc14b8f27ecfc73215be98b2.png

我们再来算下,这样一个位图结构的值对象占据多少空间。每一个位是1bit,一亿用户就是一亿bit。8bit=1Byte

100000000/8/1024/1024=11.92M

我用测试工程往一个key里通过lua塞进了1亿个bit,然后用rdb tools对内存进行统计,实测如下:

9fc13b3b4b4d0a8e271777014e4a4187.png

一天1亿用户也就消耗12M的内存空间。这完全符合要求。1年的话也就4个G。几年下来的话,redis可以集群部署来进行扩容存储。我们也可以用位图压缩算法对bitmap进行压缩存储。例如WAH,EWAH,Roaring Bitmaps。这个以后可以单独拉出来聊聊。

我们把每一天1亿用户的登陆状态都用bitmap的形式存进了redis,那要获取某一天id为88000的用户是否活跃,直接使用getbit命令:

getbit 2020-01-01 88000 [时间复杂度为O(1)]

如果要统计某一天的所有的活跃用户数,使用bitcount命令,bitcount可以统计1的个数,也就是活跃用户数:

bitcount 2019-01-01 [时间复杂度为O(N)]

如果要统计某一段时间内的活跃用户数,需要用到bitop命令。这个命令提供四种位运算,AND(与),(OR)或,XOR(亦或),NOT(非)。我们可以对某一段时间内的所有key进行OR(或)操作,或操作出来的位图是0的就代表这段时间内一次都没有登陆的用户。那只要我们求出1的个数就可以了。以下例子求出了2019-01-01到2019-01-05这段时间内的活跃用户数。

bitop or result 2019-01-01 2019-01-02 2019-01-03 2019-01-04 2019-01-05 [时间复杂度为O(N)]

bitcount result

从时间复杂度上说,无论是统计某一天,还是统计一段时间。在实际测试时,基本上都是秒出的。符合我们的预期。

3

bitmap可以很好的满足一些需要记录大量而简单信息的场景。所占空间十分小。通常来说使用场景分2类:

1.某一业务对象的横向扩展,key为某一个业务对象的id,比如记录某一个终端的功能开关,1代表开,0代表关。基本可以无限扩展,可以记录2^32个位信息。不过这种用法由于key上带有了业务对象的id,导致了key的存储空间大于了value的存储空间,从空间使用角度上来看有一定的优化空间。

2.某一业务的纵向扩展,key为某一个业务,把每一个业务对象的id作为偏移量记录到位上。这道面试题的例子就是用此法来进行解决。十分巧妙的利用了用户的id作为偏移量来找到相对应的值。当业务对象数量超过2^32时(约等于42亿),还可以分片存储。

看起来bitmap完美的解决了存储和统计的问题。那有没有比这个更加省空间的存储吗?

答案是有的。

4

redis从2.8.9之后增加了HyperLogLog数据结构。这个数据结构,根据redis的官网介绍,这是一个概率数据结构,用来估算数据的基数。能通过牺牲准确率来减少内存空间的消耗。

我们先来看看HyperLogLog的方法

PFADD 添加一个元素,如果重复,只算作一个

PFCOUNT 返回元素数量的近似值

PFMERGE 将多个 HyperLogLog 合并为一个 HyperLogLog

这很好理解,是不是。那我们就来看看同样是存储一亿用户的活跃度,HyperLogLog数据结构需要多少空间。是不是比bitmap更加省空间呢。

我通过测试工程往HyperLogLog里PFADD了一亿个元素。通过rdb tools工具统计了这个key的信息:

ef9d563038065cf4e9eda7b86c3d3c56.png

只需要14392 Bytes!也就是14KB的空间。对,你没看错。就是14K。bitmap存储一亿需要12M,而HyperLogLog只需要14K的空间。

这是一个很惊人的结果。我似乎有点不敢相信使用如此小的空间竟能存储如此大的数据量。

接下来我又放了1000w数据,统计出来还是14k。也就是说,无论你放多少数据进去,都是14K。

查了文档,发现HyperLogLog是一种概率性数据结构,在标准误差0.81%的前提下,能够统计2^64个数据。所以 HyperLogLog 适合在比如统计日活月活此类的对精度要不不高的场景。

HyperLogLog使用概率算法来统计集合的近似基数。而它算法的最本源则是伯努利过程。

伯努利过程就是一个抛硬币实验的过程。抛一枚正常硬币,落地可能是正面,也可能是反面,二者的概率都是 1/2 。伯努利过程就是一直抛硬币,直到落地时出现正面位置,并记录下抛掷次数k。比如说,抛一次硬币就出现正面了,此时 k 为 1; 第一次抛硬币是反面,则继续抛,直到第三次才出现正面,此时 k 为 3。

对于 n 次伯努利过程,我们会得到 n 个出现正面的投掷次数值 k1, k2 ... kn , 其中这里的最大值是k_max。

根据一顿数学推导,我们可以得出一个结论: 2^{k_ max} 来作为n的估计值。也就是说你可以根据最大投掷次数近似的推算出进行了几次伯努利过程。

5

虽然HyperLogLog数据类型这么牛逼,但终究不是精确统计。只适用于对精度要求不高的场景。而且这种类型无法得出每个用户的活跃度信息。毕竟只有14K嘛。也不可能存储下那么多数量的信息。

总结一下:对于文章开头所提到的面试题来说,用bitmap和HyperLogLog都可以解决。

bitmap的优势是:非常均衡的特性,精准统计,可以得到每个统计对象的状态,秒出。缺点是:当你的统计对象数量十分十分巨大时,可能会占用到一点存储空间,但也可在接受范围内。也可以通过分片,或者压缩的额外手段去解决。

HyperLogLog的优势是:可以统计夸张到无法想象的数量,并且占用小的夸张的内存。 缺点是:建立在牺牲准确率的基础上,而且无法得到每个统计对象的状态。

我做了一个演示工程redis-bit,放在Gitee上,工程包括了初始化大容量的数据。和分别使用bitmap和HyperLogLog进行用户活跃度的统计。最后通过http的方式进行输出。

工程采用springboot+redisson客户端。所有的参数支持配置

3b794737c0e38eba545408a442053b14.png

分享

51bf6e67da3845472d9078ae4ba0299c.png
00efb051a0dd9b9ae2112fffeef26ad7.png

上面是本人在学习路上整理的一些比较干货的java资料,如果有需要的兄弟可以先关注我,私信我回复【资料】即可。

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

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

相关文章

HBase查询优化

1.概述 HBase是一个实时的非关系型数据库,用来存储海量数据。但是,在实际使用场景中,在使用HBase API查询HBase中的数据时,有时会发现数据查询会很慢。本篇博客将从客户端优化和服务端优化两个方面来介绍,如何提高查询…

全国计算机等级考试题库二级C操作题100套(第49套)

第49套: 给定程序中,函数fun的功能是:将形参s所指字符串中所有ASCII码值小于97 的字符存入形参t所指字符数组中,形成一个新串,并统计出符合条件的字符个数 作为函数值返回。 例如,形参s所指的字符串为&…

NEC SV8100电话交换机配置梓博电话计费系统

为了节约办公成本,规范电话使用。公司最近为NEC SV8100电话交换机系统上线了一套梓博的电话计费系统。先将配置过程分享给各位。 1、登录电话交换机系统(默认用户名tech、密码12345678) 2、点击系统数据配置按钮 2、通过10-01选项调整电话交换…

判断闰年 php,PHP怎么判断一年是否为闰年?

判断是否为闰年的条件是满足下列二者条件之一:年号能被4整除、但不能被100整除,二是年号能被4整除,又能被400整除。那么PHP怎么判断一年是否为闰年?下面本篇文章就来给大家介绍一下使用PHP判断一年是否为闰年的方法,希…

redis desktop manager_面试官:Redis分布式锁如何解决锁超时问题?

Java面试笔试面经、Java技术每天学习一点Java面试关注不迷路作者:wangzaiplus来源:https://www.jianshu.com/u/8cb4591440ca一、前言关于redis分布式锁, 查了很多资料, 发现很多只是实现了最基础的功能, 但是, 并没有解决当锁已超时而业务逻辑还未执行完…

全国计算机等级考试题库二级C操作题100套(第50套)

第50套: 给定程序中,函数fun的功能是:有NN矩阵,以主对角线为对称线,对称元素相加并将结果存放在左下三角元素中,右上三角元素置为0。例如,若N3,有下列矩阵: 1 2 3 4 5 6 7 8 9 计算…

《CLR via C#》读书笔记 之 参数

第九章 参数 2013-02-27 9.3 以传引用的方式向方法传递参数 默认情况下,CLR假定所有方法参数都是传值的。当传递引用类型的对象时,也默认是传值的,只不过这个值是引用(指针)本身。 CLR允许以传引用的方式传递参数。在C…

学习笔记整理之模式化方法

第一步 要分清要用那个不变的参数去实现 (比如我现在要用 ID进行验证 则,验证完毕后实现的抽象方法的参数是 操作数,所以操作 的方法的参数是操作数)先new 个要实现的方法比如 New StudentManger(id,name) 此方法要把继承的抽象的…

php组合查询,PHP组合查询多条件查询实例代码第1/2页

先向大家说明需求:按照我们系统的要求,我们将通过部门名称、员工姓名、PC名称、IP地址等等字段来进行组合查询从而得到想要的数据结果。那么,为了简单起见,我们用两个条件(部门名称、员工姓名)的组合查询来向大家说明这一技术技巧…

python print 换行_Python学习 | Python的基础语法

Python 语言与 Perl,C 和 Java 等语言有许多相似之处。但是,也存在一些差异,编写Paython程序之前需要对语法有所了解,才能编写规范的Python程序。一、行和缩进Python最大的特点之一就是Python 的代码块不使用大括号 {}了&#xff…

解决linux下source /etc/profile关闭终端失效问题

本来想配置环境变量的,看网上和博客上很多说改/etc/profile,然后source /etc/profile之后就可以永久保存使环境变量生效,但是终端一关闭,就环境变量就失效了,其他终端也用不了。网上有说在当前用户目录下创建.bash_pro…

bind php,PHP – bind_result到数组

我正在为一个返回多个结果的查询使用一个预准备语句,我想在一个数组中使用它.但是bind_result不能用于数组,所以我就是这样做的:$read_items $db->stmt_init();$read_items->prepare("SELECT item_id, item_name FROM items");$read_items->exe…

SQL Server遍历表中记录的2种方法

SQL Server遍历表一般都要用到游标,SQL Server中可以很容易的用游标实现循环,实现SQL Server遍历表中记录。本文将介绍利用使用表变量和游标实现数据库中表的遍历。 表变量来实现表的遍历 以下代码中,代码块之间的差异已经用灰色的背景标记。…

全国计算机等级考试题库二级C操作题100套(第51套)

第51套: 给定程序中,函数fun的功能是:计算出形参s所指字符串中包含的单词个数, 作为函数值返回。为便于统计,规定各单词之间用空格隔开。 例如,形参s所指的字符串为:This is a C language program.&#x…

python 创建文件_Python入学首次项目,新手必看,简单易操作

继昨天文章python软件pycharm安装教程之后,今天则给新手小白们分享一哈,怎么制作并创建文件。print “hello world”;如后期需要资料文件的则可以私信留言,领取首次项目资料。本节知识点:python项目的创建pycharm的使用…

php柱形图 数据sql,ThinkPHP 5.1 读取数据库中的图片

如果一个图片直接存在数据库中,可以用以下方法读出来。环境ThinkPHP 5.1 ,sqlsrv,pdo_sqlsrv代码//pdo 方式$pdonew PDO(sqlsrv:Serverlocalhost;DatabaseSD31022_Sample, sa, Sql2008);$stmt$pdo->prepare(select picture from crm_affixinfo where …

Babelfish

题目描述 You have just moved from Waterloo to a big city. The people here speak an incomprehensible dialect of a foreign language. Fortunately, you have a dictionary to help you understand them.输入 Input consists of up to 100,000 dictionary entries, follo…

ajax跨域实现

2019独角兽企业重金招聘Python工程师标准>>> 我们都知道ajax是不能跨域的,那么怎么实现ajax跨域呢? 看了看jquery,当然,jquery封装的很好,$.ajax就可以实现跨域,只需要在参数中配置一下即可&am…

全国计算机等级考试题库二级C操作题100套(第52套)

第52套: 给定程序中,函数fun的功能是:将NN矩阵中元素的值按列右移1个位置, 右边被移出矩阵的元素绕回左边。例如,N3,有下列矩阵 1 2 3 4 5 6 7 8 9 计算结果为 3 1 2 6 4 5 9 7 8 请在程序的下划线处填入正…

python leetcode_leetcode 刷题经验,主力 python

1. 树的先序遍历可以求高度,后序遍历可以求深度。剑指 Offer 55 - II. 平衡二叉树​leetcode-cn.com2. 二叉搜索树的中序遍历可以递增地返回所有元素。逆序的中序遍历(即先右子节点,再根节点,再左子节点)可以递减的返回…