前言不知道大家大规模使用过Redis吗?或者它只是一个缓存工具?Redis中使用最多的是集合。例如以下场景:在签到系统中,一天对应一系列的用户签到记录。在电子商务系统中,一个产品对应一系列的评论。在约会系统中,用户的一系列朋友。Redis中集合的特性无非就是一个Key对应一系列数据,但是数据的作用往往是为了统计,比如:在好友系统中,需要统计每天新增的好友,如以及双方共同的朋友。在电子商务系统中,需要对评论列表中的最新评论进行统计。在签到系统中,需要统计连续一个月签到的用户数。在大型互联网应用中,数据量巨大,别说是百万级、千万级,甚至亿级,比如电商巨头淘宝、交友巨头微信、微博;办公巨头钉钉等,哪个拥有数亿用户??只需要针对不同的场景选择合适的集合,统计起来就更方便了。聚合统计聚合统计是指多个元素聚合后的结果,比如统计多个集合的交集、并集、差集。当需要对多个集合进行聚合统计时,Set集合是一个不错的选择,除了没有重复的数据外,Redis也提供了相应的API交集。在上面的例子中,交友系统中双方统计的共同好友是聚合统计中的交集。在Redis中,可以使用userid作为key,使用好友的userid作为value,如下图:统计两个用户的共同好友,只需要两个Set集合的交集.命令如下;SINTERSTOREuserid:newuserid:20002userid:20003:new此键将存储userid:20002和userid:20003的交集。以区别为例:假设交友系统需要统计每天新增的好友。这时候就需要把这两天朋友圈套的差值拿过来了。比如2020年11月1日的朋友是set1,而2020年11月1日/2日的朋友是set2,此时只需要做set1和set2的区别。这个时候应该怎么设计结构呢?如下图所示:userid:20201101这个key记录了userid用户在2020年11月1日的好友集合。差异集很简单,执行SDIFFSTORE命令即可,如下:SDIFFSTOREuser:newuserid:20201102userid:20201101执行后,此时的user:new集合为2020年11月2日新加的好友,这里换一个更合适的例子。微博有个可能认识的人的功能,可以用差分集,就是你朋友的朋友减去你们共同的朋友就是你可能认识的人。并集或差集的例子,假设你需要统计2020/11/01和2020/11/2新增的好友总数,此时只需要将新增好友的集合做并集即可在这两天。命令如下:SUNIONSTOREuserid:newuserid:20201102userid:20201101此时新建的collectionuserid:new是两天新加的好友。综上所述,Set集合的交并并集的计算复杂度非常高。如果数据量很大,Redis可能会被阻塞。那么如何避免阻塞呢?建议如下:在Redis集群中选择一个从库负责聚合统计,这样主库和其他从库不会被阻塞。数据交给客户端,客户端进行聚合统计。排序统计在某些电子商务网站上,您可以看到产品评论始终是最新的。这是怎么做到的?最新的评论列表包含了所有的评论,这就需要集合按顺序存储元素。也就是说,集合中的元素必须按顺序存储,称为有序集合。Redis中的四种集合中,List和SortedSet是有序集合。但是List和SortedSet有什么区别呢?使用哪一个?List是按照元素输入的先后顺序排序的,而SortedSet是可以按照元素权重排序的。例如,可以根据元素插入集合的时间来确定权重,先插入的元素权重小,后插入的元素权重大。对于这个例子,显然这两个都可以满足要求,List中的分页查询命令LRANGE和SortedSet中的分页查询命令ZRANGEBYSCORE。但是就灵活性而言,List肯定是不适合的。List只能按照插入的先后顺序进行排序,但是在大多数场景下,可能不仅仅按照时间排序,还可能按照一些特定的条件进行排序。这时候,SortedSet就很适合了,你只需要根据独特的算法生成相应的权重即可。二进制状态统计二进制状态是指0或1的值;在签到场景中,只需要记录两种状态:签到(1)和未签到(0),这是典型的二进制状态统计。二进制状态的统计可以使用Redis的扩展数据类型Bitmap,底层使用String类型实现,可以看做是一个位数组。后续介绍详细内容...在签到统计中,0和1只占1位,即使一年的签到数据也只有365位。大大减少了存储空间。Bitmap提供了GETBIT/SETBIT操作,使用一个偏移值offset来读写位数组的某个位。但是需要注意的是,Bitmap的offset是从0开始计算的,也就是说offset的最小值是0。当使用SETBIT写入一个bit时,该bit被设置为1。Bitmap也提供了BITCOUNT操作,用来统计这个位数组中全1的个数。如何设计键值?key可以是userid:yyyyMM,即唯一id加上月份。假设员工ID为10001,需要统计2020年11月的签到和签到记录,第一步执行命令设置值。假设你是11月2日签到,命令如下:SETBITuserid:10001:20201111BitMap从下标0开始,所以2日下标为1,值设置为1表示签到成功。第二步,查看用户是否在11月2日签到,命令如下:GETBITuserid:10001:2020111第三步,统计11月的签到次数。命令如下:GETBITuserid:10001:2020111那么问题来了,需要统计你这个签到系统连续20天签到的用户总数如何处理?假设有1亿用户。比如需要统计从2020/11/01到2020/11/20连续签到的人数,如何统计?Bitmap还支持同时对多个BitMap进行按位与、或、异或运算。命令如下图所示:思路来了,我们可以用每一天的日期作为key,对应的BitMap存储了当天1亿用户的签到情况。如下图所示:此时我们只需要对2020/11/1到2020/11/20的Bitmap进行按位与运算,最终得到的Bitmap中每个位位置对应的值代表连续20天签到,只有连续20天所有签到都签到,该位的值才为1。如下图所示:最后可以使用BITCOUNT命令进行统计。您可以尝试计算内存开销。每天使用1亿位的Bitmap大约占内存12MB(10^8/8/1024/1024)。一个20天的Bitmap内存开销约为240MB,还不包括内存压力。太大。但是在实际应用中,最好给Bitmap设置一个过期时间,让Redis自动删除不再需要的签到记录,以节省内存开销。如果涉及二进制状态,比如用户是否存在、签到、商品是否存在等,可以使用Bitmap,可以有效节省内存空间。基数统计基数统计是指计算集合中唯一元素的数量。例如:电子商务网站通常需要统计每个网页的UV来确定权重。网页的UV必须去重。在Redis类型中,Set支持去重。首先想到的是Set。但是这里有一个问题。Set的底层使用哈希表和整数数组。如果一个网页的UV达到千万级(电商网站不止一个页面),内存消耗会非常高。Redis提供了一个扩展类型的HyperLogLog用于基数统计。计算2^64个元素只需要12KB左右的内存空间,是不是很心动?但是HyperLogLog有误差,误差在0.81%左右。如果需要准确的统计,还是需要用到Set。对于这种网页的UV,已经足够了。统计网页UVs时,只需要在HyperLogLog中存储用户的唯一id,如下:PFADDp1:uv10001100021000310004如果有重复的元素,会自动去重。统计也很简单,使用PFCOUNT命令,如下:PFCOUNTp1:uv总结本文介绍了几种统计类型以及应该用什么集合来存储。为了方便理解,笔者将支持情况和优缺点总结在一张表中,如下图所示:Set和SortedSet支持交集和并集的聚合操作,但SortedSet不支持差集操作。Bitmap还可以对多个Bitmap进行AND、XOR或聚合操作。List和SortedSet都支持排序统计,但是List是按照元素的插入顺序排序的,而SortedSet支持权重,比List排序更加灵活。对于二进制状态统计,判断某个元素是否存在等,推荐使用Bitmap来节省内存空间。对于基数统计,当数据量较大,对精度要求不高时,推荐使用HyperLogLog,以节省内存空间;要获得准确的基数统计信息,最好使用Set集合。
