Redis 如何存储上亿级别的用户状态?

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 前段时间,在网上看到一道面试题:

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


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


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


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


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

image.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亿用户的活跃状态。

image.png

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


100000000/8/1024/1024=11.92M


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

image.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的信息:

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


另外,关注公众号Java技术栈,在后台回复:面试,可以获取我整理的 Redis 系列面试题和答案,非常齐全。 近期热文推荐:

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
77 6
|
3月前
|
存储 缓存 NoSQL
【Azure Redis 缓存】关于Azure Cache for Redis 服务在传输和存储键值对(Key/Value)的加密问题
【Azure Redis 缓存】关于Azure Cache for Redis 服务在传输和存储键值对(Key/Value)的加密问题
|
11天前
|
存储 NoSQL 算法
Redis分片集群中数据是怎么存储和读取的 ?
Redis集群采用哈希槽分区算法,共有16384个哈希槽,每个槽分配到不同的Redis节点上。数据操作时,通过CRC16算法对key计算并取模,确定其所属的槽和对应的节点,从而实现高效的数据存取。
40 13
|
12天前
|
存储 NoSQL Redis
【赵渝强老师】Redis的存储结构
Redis 默认配置包含 16 个数据库,通过 `databases` 参数设置。每个数据库编号从 0 开始,默认连接 0 号数据库,可通过 `SELECT <dbid>` 切换。Redis 的核心存储结构包括 `dict`、`expires` 等字段,用于处理键值和过期行为。添加键时需指定数据库信息。视频讲解和代码示例详见内容。
|
2月前
|
存储 NoSQL Redis
2)Redis 的键值对长什么样子,又是怎么存储的?
2)Redis 的键值对长什么样子,又是怎么存储的?
43 0
|
3月前
|
存储 消息中间件 NoSQL
Redis命令详解以及存储原理
Redis命令详解以及存储原理
|
3月前
|
存储 NoSQL Redis
Redis存储原理与数据模型
Redis存储原理与数据模型
|
3月前
|
存储 缓存 NoSQL
Redis深度解析:部署模式、数据类型、存储模型与实战问题解决
Redis深度解析:部署模式、数据类型、存储模型与实战问题解决
下一篇
无影云桌面