Redis如何存储和计算一亿用户的活跃度
1
前段时间,在网上看到一道面试题:
如何用redis存储统计1亿用户一年的登陆情况,并快速检索任意时间窗口内的活跃用户数量。
觉得很有意思,就仔细想了下 。并做了一系列实验,自己模拟了下 。还是有点收获的,现整理下来。和大家一起分享。
Redis是一个内存数据库,采用单线程和事件驱动的机制来处理网络请求。实际生产的QPS和TPS单台都能达到3,4W,读写性能非常棒。用来存储一些对核心业务弱影响的用户状态信息还是非常不错的。
对于这题,有2个重要的点需要考虑:
1.如何用合适的数据类型来存储1亿用户的数据,用普通的字符串来存储肯定不行。经过查看一个最简单的kv(key为aaa,value为1)的内存占用,发现为48byte。
假设每个用户每天登陆需要占据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亿用户的活跃状态。
我们再来算下,这样一个位图结构的值对象占据多少空间。每一个位是1bit,一亿用户就是一亿bit。8bit=1Byte
100000000/8/1024/1024=11.92M
我用测试工程往一个key里通过lua塞进了1亿个bit,然后用rdb tools对内存进行统计,实测如下:
一天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的信息:
只需要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客户端。所有的参数支持配置
6
微信关注「jishuyuanren」获取更多干货,或者扫描以下二维码
本文由博客群发一文多发等运营工具平台 OpenWrite 发布
Redis如何存储和计算一亿用户的活跃度的更多相关文章
- 替代或者与 Redis 配合存储十亿级别列表的数据.
http://ssdb.io/docs/zh_cn/index.html 用户案例 如果你在生产环境中使用 SSDB, 欢迎你给我发邮件(ssdb#udpwork.com), 我很愿意把你加入到下面的 ...
- 巧用redis位图存储亿级数据与访问 - 简书
原文:巧用redis位图存储亿级数据与访问 - 简书 业务背景 现有一个业务需求,需要从一批很大的用户活跃数据(2亿+)中判断用户是否是活跃用户.由于此数据是基于用户的各种行为日志清洗才能得到,数据部 ...
- 快手推荐系统及 Redis 升级存储
快手推荐系统及 Redis 升级存储 借傲腾 补上 DRAM 短板 内容简介: 作为短视频领域的领先企业,快手需要不断导入更先进的技术手段来调整和优化其系统架构,以应对用户量和短视频作品数量的爆炸式 ...
- 阿里云POLARDB如何助力轻松筹打造5亿用户信赖的大病筹款平台?
轻松筹首创了“大病救助”模式,帮助了众多病患在第一时间解決了医疗资金等问题,为了从源头解决了医疗资金问题.而在轻松筹这样全球5.5亿用户信赖的大病筹款平台的背后,是日益增长的各种数据.面对这样数据量所 ...
- Redis混合存储-冷热数据识别与交换
Redis混合存储产品是阿里云自主研发的完全兼容Redis协议和特性的混合存储产品. 通过将部分冷数据存储到磁盘,在保证绝大部分访问性能不下降的基础上,大大降低了用户成本并突破了内存对Redis单实例 ...
- 腾讯云Redis混合存储版重磅推出,万字长文助你破解缓存难题!
导语 | 缓存+存储的系统架构是目前常见的系统架构,缓存层负责加速访问,存储层负责存储数据.这样的架构需要业务层或者是中间件去实现缓存和存储的双写.冷热数据的交换,同时还面临着缓存失效.缓存刷脏.数据 ...
- Redis内存存储结构分析
1 Redis 内存存储结构 本文是基于 Redis-v2.2.4 版本进行分析. 1.1 Redis 内存存储总体结构 Redis 是支持多key-value数据库(表)的,并用 RedisDb 来 ...
- Redis 混合存储最佳实践指南
Redis 混合存储实例是阿里云自主研发的兼容Redis协议和特性的云数据库产品,混合存储实例突破 Redis 数据必须全部存储到内存的限制,使用磁盘存储全量数据,并将热数据缓存到内存,实现访问性能与 ...
- 大数据软件安装之Hadoop(Apache)(数据存储及计算)
大数据软件安装之Hadoop(Apache)(数据存储及计算) 一.生产环境准备 1.修改主机名 vim /etc/sysconfig/network 2.修改静态ip vim /etc/udev/r ...
随机推荐
- cc32b_demo-32dk2j_cpp_纯虚函数与抽象类2-txwtech
cc32b_demo-32dk2j_cpp_纯虚函数与抽象类2-txwtech //纯虚函数是用来继承用的//纯虚函数//抽象类-抽象数据类型//*任何包含一个或者多个纯虚函数的类都是抽象类//*不要 ...
- vs.net/vscode中使用Beetlex创建vue应用
平时在开发Vue应用则需要安装nodejs,vue cli等相关东西相对来说麻烦一些:如果你喜欢像vs.net/vscode创建普通项目一样就能开发Vue项目的话那可以尝试一下BeetleX针对Vue ...
- 多语言工作者の十日冲刺<8/10>
这个作业属于哪个课程 软件工程 (福州大学至诚学院 - 计算机工程系) 这个作业要求在哪里 团队作业第五次--Alpha冲刺 这个作业的目标 团队进行Alpha冲刺--第八天(05.07) 作业正文 ...
- Magicodes.IE在.NET Core中通过请求头导出多种格式文件
前言 在2.2里程碑中我们增加了一些新的功能,正如标题所写通过请求头进行导出我们不同格式的文件.下面我们来看一下如何使用.通过这种方式无论是对我们的数据多用途,还是说对我们的数据校验都做到了轻松易配. ...
- Area.js下载
因为vant AddressEdit 地址编辑的必要组件area.js网站经常进不去,所以存在这里,area.js 代码如下: export default { province_list: { 11 ...
- Blazor带我重玩前端(一)
写在前面 曾经我和前端朋友聊天的时候,我说我希望有一天可以用C#写前端,不过当时更多的是美好的想象,而现在这一切正变得真实…… 什么是Blazor 我们知道浏览器可以正确解释并执行JavaScript ...
- mysql replace替换某字段的值
由于最近我们的一个网站回购了一个很好的域名所有与之相关的项目都需要修改: 今天接到一个任务将我们会员开通的个人网站的二级域名换成新域名,看了一下库已开通的还很少才2w多,且要换的和之前库中的数据很规则 ...
- Python实用笔记 (24)面向对象高级编程——使用@property
这显然不合逻辑.为了限制score的范围,可以通过一个set_score()方法来设置成绩,再通过一个get_score()来获取成绩,这样,在set_score()方法里,就可以检查参数: clas ...
- 新建maven项目总是需要重新选择maven的配置文件
解决办法: other settings->settings for new projects... 找到maven设置自己的目录和配置
- scala数据结构(一)
一.概述 1,特点 )Scala同时支持不可变集合和可变集合 )两个主要的包: 不可变集合:scala.collection.immutable 可变集合: scala.collection.muta ...