当前位置: 首页 > 科技观察

Redis实战篇:使用数据类型实现亿级数据统计

时间:2023-03-12 08:24:13 科技观察

在移动端应用的业务场景中,我们需要保存这样的信息:一个key关联一个数据集,同时,其中的数据该集合必须经过统计排序。常见的场景如下:给一个userId,判断用户的登录状态;2亿用户近7天签到情况,统计7天内连续签到的用户总数;统计每天新增用户数和次日留存用户数;统计网站用户数List最新评论访客数(UniqueVisitor,UV)根据播放次数的音乐榜单通常情况下,我们面对的用户数和访问量都是巨大的,比如百万级、千万级、千万级、甚至亿级的用户访问信息量。因此,我们必须选择一种能够非常高效地统计大量数据(例如数十亿)的集合类型。如何选择合适的数据集,首先要了解常用的统计模型,用合理的数据解决实际问题。四种统计类型:二进制状态统计;汇总统计;排序统计;基数统计。本文将使用String、Set、Zset、List、hash以外的扩展数据类型Bitmap、HyperLogLog来实现。二进制统计可以参考历史文章:Redis实战篇:使用Bitmap实现海量数据统计今天我们来看一下剩下的三种统计。文中涉及的指令可以通过在线Redis客户端运行调试,地址:https://try.redis.io/,超级方便。CardinalitystatisticsCardinalitystatistics:统计集合中唯一元素的数量,常用于计算唯一用户数(UV)。实现基数统计最直接的方法就是采用集合(Set)的数据结构。当一个元素从未出现过时,将在集合中添加一个元素;如果它已经出现,则该集合将保持不变。当页面访问量巨大时,需要一个非常大的Set集合来统计,会浪费很多空间。此外,此类数据不需要非常准确。有更好的解决方案吗?这是一个很好的问题。Redis提供了HyperLogLog数据结构来解决各种场景下的统计问题。HyperLogLog是一种不精确的重复数据删除基数方案。其统计规则是基于概率实现的,标准误差为0.81%。这个精度足以满足UV统计要求。HyperLogLog的原理太复杂了。如果想了解更多,请前往:https://www.zhihu.com/question/53416615https://en.wikipedia.org/wiki/HyperLogLog网站的UV通过SetMultiplevisits实现一天一个用户到一个网站只能统计一次,所以很容易想到通过Redis的Set集合来实现。当用户ID89757访问“Redis为什么这么快”时,我们将这些信息放在Set中。SADRedis为什么这么快:uv89757当89757号用户多次访问“Redis为什么这么快”页面时,Set的去重功能可以保证同一个用户ID不会被重复记录。使用SCARD命令统计《Redis为什么这么快》的页面UVs。该命令返回集合中元素的数量(即用户ID)。SCARDRedis为什么这么快:uv通过Hash实现老湿代码,也可以使用Hash类型来实现,使用用户ID作为Hash集合的key,访问时执行HSET命令将value设置为1这一页。即使用户反复访问,反复执行命令,这个userId的值也只会被设置为“1”。最后使用HLEN命令统计Hash集合中的元素个数就是UV。如下:HSETrediscluster:uvuserId:897571//统计UVHLENredis集群HyperLogLog大王的程序代码又老又湿,Set虽然好,如果文章很火,达到千万级,一个Set省几千万用户ID,页面会消耗更多内存也太大。同样,Hash数据类型也是一样的。我应该怎么办?使用Redis提供的HyperLogLog高级数据结构(不要只知道Redis的五种基本数据类型)。这是一种用于基数统计的数据收集类型。即使数据量很大,计算基数所需的空间也是固定的。每个HyperLogLog只需要花费最多12KB的内存来计算2的64次方元素的基数。Redis优化了HyperLogLog的存储。当计数比较小时,存储空间采用系数矩阵,占用空间小。只有当count很大,稀疏矩阵占用的空间超过阈值时,才会转化为稠密矩阵,占用12KB的空间。PFADD将访问页面的每个用户ID添加到HyperLogLog。PFADDRedis主从同步原理:uvuserID1userID2useID3PFCOUNT使用PFCOUNT获取《Redis主从同步原理》页面的UV值。PFCOUNTRdis主从同步原理:uvPFMERGE使用场景HyperLogLog除了上面的PFADD和PFCOIUNT,还提供了PFMERGE来组合多个HyperLogLog形成一个新的HyperLogLog值。语法PFMERGEdestkeysourcekey[sourcekey...]场景比如我们网站有两个内容相似的页面,操作说需要合并这两个页面的数据。页面的UV访问也需要合并,这时PFMERGE就可以派上用场了,即同一个用户只访问这两个页面一次。如下图:Redis和MySQL的两个Bitmap集合分别存放两个页面的用户访问数据。PFADDRedisdatauser1user2user3PFADDMySQLdatauser1user2user4PFMERGEdatabaseRedisdataMySQLdataPFCOUNTdatabase//returnvalue=4将多个HyperLogLog合并(merge)成一个HyperLogLog,合并后的Hy??perLogLog的基数接近输入到HyperLogLog集合的所有观察集的并集。user1和user2都访问过Redis和MySQL,只统计一次访问。排序统计Redis的4种集合类型(List、Set、Hash、SortedSet)中,List和SortedSet是有序的。List:按照元素插入List的先后顺序排序。使用场景通常可以作为消息队列、最新列表、排行榜;SortedSet:按照元素的得分权重排序,我们可以自行确定每个元素的权重值。使用场景(排行榜,比如浏览量和点赞量)。最新的评论列表代码又旧又湿。我可以利用List插入的排序顺序来实现一个评论列表,比如微信后台回复列表公众号(不要用bar,例如)。每一个公众号对应一个List,这个List保存了所有用户对的评论。每当有用户评论时,使用LPUSHkeyvalue[value...]插入到List队列的头部。LPUSHcode兄弟byte123456然后使用LRANGEkeystarstop获取列表指定区间内的元素。>LRANGEcodebyte041)"6"2)"5"3)"4"4)"3"5)"2"注意不是所有的最新列表都能用List实现,对于经常更新的List,列表的分页type可能导致列表元素重复或遗漏。比如当前评论列表List={A,B,C,D},左边代表最新的评论,D是最早的评论。LPUSH码字节兄DCBA第一页显示最新2条评论,得到A和B:LRANGE码字节兄011)"A"2)"B"按照我们要的逻辑,第二页可以传LRANGE码brotherbyte23getC,D如果在第二页显示之前有新的评论E产生,则通过LPUSH码字节E将评论E插入List队列的头部,List={E,A,B,C,D}。现在执行LRANGE代码brotherbyte23获取第二页的评论,发现B又出现了。LRANGE码兄弟byte231)"B"2)"C"出现这种情况的原因是List是按照元素的位置排序的。插入新元素后,List={E,A,B,C,D}。原始数据在List中的位置向后移动了一位,导致读取的是old元素。Listlatestlistsummary只有不需要分页的列表(比如每次只取列表的前5个元素)或者更新频率低的(比如每天早上更新一次统计信息)才适合实现使用列表类型。对于需要分页并且会经常更新的列表,需要使用SortedSet类型来实现。另外需要通过时间范围查找最新列表,List类型是无法实现的。需要通过SortedSet类型来实现,比如以交易时间范围为条件查询的订单列表。排行榜代码又旧又湿。最新的列表场景,List和SortedSet都可以实现。为什么要使用列表?直接用SortedSet不是更好,还可以更灵活的设置分数权重排序。原因是SortedSet类型占用的内存容量是List类型的数倍。对于列表数量较少的情况,可以使用SortedSet类型来实现。比如我们要一个一周的音乐列表,需要实时更新播放量,需要分页显示。另外,排序是根据播放量来决定的,此时不能满足List。我们可以将音乐ID保存到SortedSet集合中,将score设置为每首歌曲的播放音量,每次播放音乐时设置score=score+1。ZADD比如我们把《青花瓷》和《花田错》的播放音量加入到musicTop集合中:ZADDmusicTop100000000青花瓷8999999花田错ZINCRBY《青花瓷》每次播放都会通过ZINCRBY指令得分+1。>ZINCRBYmusicTop1青花瓷100000001ZRANGEBYSCORE最后需要获取musicTop的前十名播放音乐列表。当前最大播放音量为N,可以通过以下命令获取:ZRANGEBYSCORemusicTopN-9NWITHSCORES65师兄:但是这个N怎么获取呢?ZREVRANGE可以通过ZREVRANGEkeystartstop[WITHSCORES]指令获得。元素按得分降序排列(从大到小)。分值相同的成员按字典序倒序排列。>ZREVRANGEmusicTop00WITHSCORES1)《青花瓷》2)100000000总结即使集合中元素更新频繁,SortedSet也可以通过ZRANGEBYSCORE命令准确获取排序后的数据。面对需要显示最新列表、排行榜等,如果数据更新频繁或者需要分页显示,建议优先使用SortedSet。聚合统计是指多个集合元素的聚合结果,例如:多个元素的公共数据统计(交集);两个集合中唯一元素的统计(差异统计);多个集合元素的所有集合的统计(联合统计)。代码又老又湿,什么样的场景会用到交、差、并?Redis的Set类型支持在集合中进行增删改查。底层使用Hash数据结构。不管是add还是remove,都是O(1)的时间复杂度。它还支持多个集合之间的交、并、差运算,并利用这些集合运算来解决上述统计问题。交集——QQ中的共同好友等共同好友,就是聚合统计中的交集。我们用账号作为Key,账号的好友作为Set集合的值。模拟两个用户的好友集:SADDuser:码哥,字节R,Linux高手之父,PHPSADDuser:Linux高手,Python高手,C++菜鸡交集统计两个用户的共同好友只需要两个Set集的交集,如下命令:SINTERSTOREuser:mutualfrienduser:codebrotherbyteuser:boss命令执行后,用户中存储“user:codebrotherbyte”和“user:boss”这两组交集数据:共同好友收藏中。差异-每天添加的新朋友数量。比如统计一个APP每天的新注册用户数,只需要取最近两天注册用户总数的差值。例如2021-06-01的注册用户总数存储在key=user:20210601集合中,2021-06-02的用户总数存储在key=user:20210602集合中。set差异集是如下命令,执行差异集计算,并将结果存放在user:new集合中。执行SDIFFSTOREuser:newuser:20210602user:20210601,此时的user:new集合就是2021年6月2日的新用户数。另外QQ上有一个可能认识的人的功能,也可以实现通过使用差分集,也就是从你朋友的朋友集中减去你共同的朋友,得到可能的熟人。Union-是否添加新好友总数或差集的例子,统计2021/06/01和2021/06/02这两天的新用户总数,只需要执行union两套。SUNIONSTOREuserid:newuser:20210602user:20210601此时新收藏的userid:new是这两天新加的好友。总结Set的差、并、交计算复杂度高。在数据量很大的情况下,如果直接进行这些计算,Redis实例会被阻塞。所以,可以部署一个专门做统计的集群,让它负责聚合计算,或者把数据读到客户端,在客户端完成聚合统计,避免其他服务无法响应。阻塞。本文转载自微信公众号“代码字节”,可通过以下二维码关注。转载本文请联系码哥字节公众号。