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客户端。所有的参数支持配置

分享

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

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

  1. Oracle中通过:触发器,存储过程,Function调用实现解析Clob字段类型中存储的xml字符串...

    摘要:最近项目中用到了存储过程,触发器,Function,由于以前没怎么用过,所以查资料,请教同事,最后总算是把问题解决了,问题是这样的,数据库中有三张表一张是存放从远程服务器获取数据的MBINMSG ...

  2. 先查询再插入的存储过程怎么写_谈一谈 InnoDB(1) - 底层存储文件结构

    看技术文章是不是很累呢, 这次来个轻松点的~来谈一谈MySQL最主流的数据库引擎 InnoDB 吧~ 序 老王走进一号会议室, 随手打开了灯, 小张紧随其后 "王哥, 找我来干啥啊" ...

  3. 未明确定义列存储过程没问题_使用Apache Kudu和Impala实现存储分层

    当为应用程序的数据选择一个存储系统时,我们通常会选择一个最适合我们业务场景的存储系统.对于快速更新和实时分析工作较多的场景,我们可能希望使用 Apache Kudu ,但是对于低成本的大规模可伸缩性场 ...

  4. mongodb 存储过程 遍历表数据_三、redis数据存储之跳跃表(SKIP LIST)

    导读 前面文章[一.深入理解redis之需要掌握的知识点 ]中,我们对redis需要学习的内容框架进行了一个梳理.[二.redis中String和List两种数据类型和应用场景 ].[二.redis中 ...

  5. Gitlab安装使用及汉化配置

    一.GitLab简介 GitHub是2008年由Ruby on Rails编写而成,与业界闻名的Github类似;但要将代码上传到GitHub上面,而且将项目设为私有还要收费.GitLab是一个用于仓 ...

  6. SQL Server 2008 - 第2章 创建、管理 数据库

    第2章  创建数据库 数据库是用来存储数据的空间,它作为存储结构的最高层次是其他一切数据库操作的基础.用户可以通过创建数据库来存储不同类别或者形式的数据. 因此,在本章用户将详细地学习针对数据库的基本 ...

  7. 详解线上gitlab服务搭建(字数2w+)

    目录 一.GitLab是什么? 1.GitLab的功能 2.GitLab和Git区别 3.GitLab的优势 二.GitLab安装1.环境配置 2.环境要求 #系统层 #Ruby versions # ...

  8. mysql query browswer_MySQL数据库新特性之存储过程入门教程

    MySQL数据库新特性之存储过程入门教程 在MySQL 5中,终于引入了存储过程这一新特性,这将大大增强MYSQL的数据库处理能力.在本文中将指导读者快速掌握MySQL 5的存储过程的基本知识,带领用 ...

  9. 数据库实验:数据库和表、查询、视图与安全性、存储过程及游标、触发器、综合实验-水果商店进阶

    数据库实验:数据库和表.查询.视图与安全性.存储过程及游标.触发器.综合实验-水果商店进阶 实验一.数据库和表 源码1: 源码2: 小结 实验二.查询 源码 小结 实验三.视图.安全性 源码: 小结 ...

最新文章

  1. grunt使用watch和livereload的Gruntfile.js的配置
  2. 抢先体验微软最新客户端系统Windows 8.1!
  3. 为什么有那么多的水,源源不断从山上流下来,到底是从哪里来的?
  4. 算法练习day8——190326(猫狗队列、转圈打印矩阵、旋转正方形矩阵、反转单向双向链表、数N的加法组合)
  5. 【app.json】配置说明,不断更新中
  6. android.os.binderproxy cannot be cast to,Android服务android.os.BinderProxy错误
  7. redux中间件原理-讲义
  8. springboot中日志配置
  9. win10系统,virtualBox导入centos7.3报错
  10. 突发!王思聪名下房产、汽车、存款等被查封
  11. java.lang.NoClassDefFoundError:
  12. [导入]正则表达式学习心得体会(2)
  13. 如何断点调试Tomcat源码
  14. mysql数据库日志太多_全方位解读 MySQL 日志实现内幕(四)
  15. 单机 搭建kafka集群 本地_单机简单搭建一个kafka集群(没有进行内核参数和JVM的调优)...
  16. 知到智慧树答案2020python_2020知到智慧树大数据分析的python基础章节答案
  17. android 在app内打开淘宝链接
  18. ABTest之最常见的八个错误
  19. 阿里云注册商标现身说法成功率打在公屏上
  20. swagger(三):统一返回结果不显示字段说明

热门文章

  1. Ubuntu Server 之Apache2 虚拟主机配置指南(个人实践解读)
  2. 现代制造工程笔记04-精密超精密加工和特种加工(主要掌握加工原理加工条件)
  3. php关注 取消关注事件,微信公众平台开发关注/取消关注事件例子
  4. 小说里的lt什么意思_游戏cpdd网络用语是什么意思 王者荣耀里很常见
  5. 的函数原型_JS基础函数、对象和原型、原型链的关系
  6. linux常用命令_Linux常用命令全称
  7. Postgresql日志配置
  8. postgresql数据库去重方法
  9. 739. Daily Temperatures - LeetCode
  10. Cookie 与Session 的区别