当前位置: 首页 > 后端技术 > Java

什么是RedisHyperLogLog?这些场景都用上了,让我射龙,笑破天

时间:2023-04-01 19:26:34 Java

在移动互联网业务场景中,数据量很大,我们需要保存这样的信息:一个key关联一个数据集,同时对数据进行统计。例如:统计某个APP的日活跃用户数和月活跃用户数;计算每天有多少不同的帐户访问一个页面(独立访客,UV));统计用户每天搜索的不同条目的数量;计算注册IP的数量。通常,我们面对的用户数量和访问量都是巨大的,比如百万级、千万级的用户量,或者是千万级甚至亿级的访问信息量。今天《码哥》使用不同的数据类型来实现:统计每天有多少不同账号访问一个页面的功能,并逐步介绍了HyperLogLog的原理,并在Java中集成了Redission。告诉你一个技巧,Redis官网现在可以在线运行Redis命令了:https://redis.io/。如图:使用Set实现用户一天访问网站多次只能统计一次,所以很容易想到通过Redis的Set集合来实现。比如当微信ID是“小菜鸡”访问《Redis为什么这么快》一文时,我们将这些信息保存在Set中。SADD为什么Redis这么快:uv小菜鸡谢八哥小菜鸡(整数)1“小菜鸡”多次访问“Redis为什么这么快”页面,Set的去重功能保证了同一个“微信ID”。使用SCARD命令统计《Redis为什么这么快》的页面UVs。该命令返回集合中元素的数量(即用户ID)。为什么SCARDRedis这么快:uv(integer)2使用Hash实现老湿代码,也可以使用Hash类型来实现,使用用户ID作为Hash集合的key,执行HSET命令设置访问页面时该值为1。即使“小菜鸡”多次访问页面,重复执行命令,也只会将等于“小菜鸡”的key的值设置为1。最后使用HLEN命令统计Hash集合中的元素个数是紫外线。如下:HSETRedis为什么这么快小菜鸡1//统计UVHLENRedis为什么这么快使用Bitmap实现Bitmap的底层数据结构使用String类型的SDS数据结构来存储位数组,Redis存储每个8字节bytearray每一位都用到,每一位代表一个元素的二进制状态(0或1)。Bitmap提供了GETBIT和SETBIT操作,通过一个偏移值offset来读写位数组偏移位置的位。需要注意的是,偏移量是从0开始的。Bitmap可以看作是一个位单元数组,数组的每个单元只能存储0或1,数组的下标在Bitmap中称为偏移量。为了直观显示,我们可以理解为buf数组的每个字节用一行来表示,每一行有8位,8个格子代表这个字节中的8位,如下图所示:8位组成一个字节,所以Bitmap会大大节省存储空间。这就是Bitmap的优势。如何使用Bitmap统计页面的唯一用户访问量?Bitmap提供SETBIT和BITCOUNT操作。前者通过一个偏移值offset将位写入位数组的偏移位置。需要注意的是,偏移量是从0开始的。后者统计给定指定位数组中value=1的位数。注意我们需要把“微信ID”转换成数字,因为offset是下标。假设我们将“小菜集”转换为代码6,第一步是执行如下命令,指明“小菜集”的代码为6,访问《使用Redis数据类型实现亿级数据统计》一文。SETBIT巧妙利用Redis数据类型实现亿级数据统计61第二步统计页面访问次数,使用BITCOUNT命令。该命令用于计算给定位数组中值为1的位数。BITCOUNT巧妙利用Redis数据类型实现亿级数据统计HyperLogLog王的解决方案Set不错,但是如果文章很火,达到几千万,一个Set就省下几千万的用户ID,内存消耗就大了如果页面太多,则太大。同样,Hash数据类型也是一样的。至于Bitmap,更适合“二进制状态统计”的使用场景,统计精度高。虽然内存占用比HashMap少,但是对于大量的数据来说还是会占用大量的内存。我应该怎么办?以上是典型的“基数统计”应用场景,基数统计:统计一个集合中唯一元素的个数。HyperLogLog的优点是它所需要的内存不会因为集合的大小而改变。无论集合包含多少个元素,HyperLogLog计算所需要的内存总是固定的,而且很小。每个HyperLogLog最多只需要花费12KB的内存,在标准误差0.81%的前提下,可以计算出2的64次方元素的基数。Redis实战HyperLogLog使用起来太简单了。PFADD、PFCOUNT、PFMERGE这三个命令独领风骚。PFADD将访问页面的每个用户ID添加到HyperLogLog。PFADDRedis主从同步原理:uvuserID1userID2useID3PFCOUNT使用PFCOUNT获取《Redis主从同步原理》一文的UV值。PFCOUNTRedis主从同步原理:uvPFMERGE使用场景HyperLogLog`除了上面的`PFADD`和`PFCOIUNT`外,还提供了`PFMERGE语法PFMERGEdestkeysourcekey[sourcekey...]比如我们在里面有两个类似的内容网站页面,操作说需要合并这两个页面的数据。页面的UV访问也需要合并,这时PFMERGE就可以派上用场了,即同一个用户只访问这两个页面一次。如下图:Redis和MySQL的两个HyperLogLog集合分别保存了两个页面的用户访问数据。PFADDRedisdatauser1user2user3PFADDMySQLdatauser1user2user4PFMERGEdatabaseRedisdataMySQLdataPFCOUNTdatabase//returnvalue=4将多个HyperLogLog合并(merge)成一个HyperLogLog,合并后的Hy??perLogLog的基数接近所有输入HyperLogLog的可见集合(观察集)联合。user1和user2都访问过Redis和MySQL,只统计一次访问。Redission实战详细源码《兄弟代码》已经上传到GitHub:https://github.com/MageByte-Z...pom依赖于org.redissonredisson-spring-boot-starter3.16.7AdddatatoLog//添加单个元素publicvoidadd(StringlogName,Titem){RHyperLogLoghyperLogLog=redissonClient.getHyperLogLog(logName);hyperLogLog.add(item);}//添加采集数据到HyperLogLogpublicvoidaddAll(StringlogName,Listitems){RHyperLogLoghyperLogLog=redissonClient.getHyperLogLog(logName);hyperLogLog.addAll(items);}Merge/***将otherLogNames的日志合并到logName中**@paramlogName当前日志*@paramotherLogNames其他需要合并到当前日志中的日志*@param*/publicvoidmerge(StringlogName,String...otherLogNames){RHyperLogLoghyperLogLog=redissonClient.getHyperLogLog(logName);}hyperLogLog.mergeWith(otherLogNames);}统计基础publiclongcount(StringlogName){RHyperLogLoghyperLogLog=redissonClient.getHyperLogLog(logName);}返回hyperLogLog.count();}单元测试@Slf4j@RunWith(SpringRunner.class)@SpringBootTest(classes=RedissionApplication.class)publicclassHyperLogLogTest{@AutowiredprivateHyperLogLogServicehyperLogLogService;@TestpublicvoidtestAdd(){StringlogName="代码:为什么Redis这么快:uv";Stringitem="小菜鸡";hyperLogLogService.add(logName,item);log.info("添加元素[{}]到日志[{}]",item,logName);}@TestpublicvoidtestCount(){StringlogName="码哥字节:为什么Redis这么快:uv";longcount=hyperLogLogService.count(logName);log.info("logName={}count={}.",logName,count);}@TestpublicvoidtestMerge(){ArrayListitems=newArrayList<>();items.add("小菜鸡");items.add("谢八哥");items.add("陈小白");StringotherLogName="字节跳动:Redis多线程模型原理与实践:uv";hyperLogLogService.addAll(otherLogName,items);log.info("Add{}元素记录[{}]。",items.size(),otherLogName);StringlogName="代码:为什么Redis这么快:uv";hyperLogLogService.merge(logName,otherLogName);log.info("Merge{}into{}.",otherLogName,logName);longcount=hyperLogLogService.count(logName);log.info("Combinedcount={}.",count);}}基本原理HyperLogLog是一种概率数据结构,它使用概率算法来计算而其算法的起源是伯努利过程,伯努利过程是一个抛硬币的实验过程,抛一枚普通的硬币,落地可能是正面,也可能是反面,两个其中之一的概率是1/2。伯努利过程是抛一枚硬币直到正面朝上,记录抛的次数k。直到第三次出现正面,此时k为3。对于n个伯努利过程,我们将得到n次正面朝上的抛掷k1,k2...kn,其中最大值为k_max。根据数学推导,我们可以得出结论:2^{k_max}作为n的估计值。也就是说,你可以根据伯努利过程的最大抛出次数来近似计算几次。所以HyperLogLog的基本思想是利用集合中数字位串的第1个出现位置的最大值来估计整体的基数,但是这种估计方法误差较大,为了提高误差在本例中,HyperLogLog引入bucketaverage的概念,计算m个buckets的调和平均值。Redis中的HyperLogLog分为2^14个桶,即16384个桶。每个桶都是一个6位的数组,如下图所示。HyperLogLog的原理太复杂了。想了解更多请移步:https://www.zhihu.com/questio...https://en.wikipedia.org/wiki...如何统计用户日活和月活——RedisHyperLogLog详解Redis优化了HyperLogLog的存储。当计数比较小时,存储空间采用系数矩阵,占用空间小。只有当count很大,稀疏矩阵占用的空间超过阈值时,才会转化为稠密矩阵,占用12KB的空间。为什么只有12KB?HyperLogLog的实现使用了16384个桶,也就是2^14。每个bucket的maxbits需要6位来存储,最大可以表示maxbits=63,所以总的内存使用量为2^14*6/8=12kbyte。综上所述,Hash、Bitmap、HyperLogLog用于实现:Hash:算法简单,统计精度高,数据量小,海量数据会占用大量内存;Bitmap:位图算法,适用于“二进制统计场景”,具体可以参考我的文章。大量不同页面的数据统计仍然会占用大量内存。Set:使用去重特性实现。一个Set可以存储几千万个用户ID,页面太多消耗的内存太大。在Redis中,每个HyperLogLogkey只需要12KB的内存就可以计算出将近2^64个不同元素的基数。因为HyperLogLog只是根据输入元素计算基数,并没有存储输入元素本身,所以HyperLogLog不能像集合一样返回输入的单个元素。HyperLogLog是一种算法,并不是Redis独有的。目的是做基数统计,所以不是集合,不会保存元数据。它只记录数量而不是价值。空间占用很小,支持超大数据量的输入。核心是基数估计算法。主要表现在内存的使用和计算时数据合并的处理上。最终值存在一定误差。Redis中每个Hyperloglogkey占用12K内存来标记基数(官方文档)。pfadd命令并不是一次性分配12K内存,而是随着基数的增加逐渐增加内存分配;而pfmerge操作会合并sourcekey并存储在一个12k大小的key中。hyperloglog合并操作的原理(合并两个Hyperloglog时需要分别比较每个bucket的值)很容易理解。误差说明:基本估计的结果是标准误差为0.81%的近似值。这是一个可以接受的范围。Redis优化了HyperLogLog的存储。当计数比较小时,存储空间用稀疏矩阵存储,占用空间小。只有当计数逐渐增加,稀疏矩阵占用的空间逐渐超过阈值时才会使用一次。属性转化成稠密矩阵才占用12k空间亿级海量数据统计Redis实战篇:通过Geo类型实现附近的人遇见女神Redis分布式锁正确实现原理演进过程及Redisson实战总结参考资料