目录
为什么出现这3个类型
示例:
手机App中的每天的⽤⼾登录信息:1天对应1系列⽤⼾ID或移动设备ID;
电商⽹站上商品的⽤⼾评论列表:1个商品对应了1系列的评论;
⽤⼾在⼿机App上的签到打卡信息:1天对应1系列⽤⼾的签到记录;
应⽤⽹站上的⽹⻚访问信息:1个⽹⻚对应1系列的访问点击
面试问
记录对集合中的数据进行统计
在移动应用中,需要统计每天的新增用户数和第2天的留存用户数;
在电商网站的商品评论中,需要统计评论列表中的最新评论;
在签到打卡中,需要统计一个月内连续打卡的用户数;
在网页访问记录中,需要统计独立访客(UniqueVisitor,UV)量。
。。。。。。
需求痛点
数据量较大的收集+统计
需要 存的进+取得快+多统计
真正有价值的是统计。。。
统计的类型有哪些-亿级系统中常见的四种统计
聚合统计
- 统计多个集合元素的聚合结果,就是前面说过的交差并等集合统计
- 交并差集的聚合函数的应用
排序统计
- 抖音视频最新评论留言的场景,请你设计一个展现列表。考察你的数据结构和设计思路
- 设计案例和回答思路
以抖音vcr最新的留言评价为案例,所有评论需要两个功能,按照时间排序+分页显示
能够排序+分页显示的redis数据结构是什么合适?
- List
每个商品评论对应一个list集合,这个list包含了对这个商品的所有评论,而且会按照评论时间保存这些评论。
每来一个新评论就用LPUSH命令把他插入list的队头。
但是,如果在展示第二页之前,又产生了一条新评论,第2页的评论不一样了。
原因:list是通过元素在list中的位置来排序的,当有一个新元素插入时,原先的元素在list中就都后移了一位,原来在1的元素现在排在了第2位,当lrange读取时,就会读到旧数据。
- Zset
- 在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页展示,建议使用zset
二值统计
集合元素的取值就只有0和1两种。
在钉钉上班签到打卡的场景中,我们只用记录有签到(1)或没签到(0)
具体见bitmap。
基数统计
指统计一个集合中不重复的元素个数
具体见hyperloglog
Bitmap
是什么
一句话:由0和1状态表现的二进制位的bit数组
说明:用string类型作为底层数据结构实现的一种统计二值状态的数据类型。
位图的本质是数组,它是基于string数据类型的按位的操作。
该数组由多个二进制位组成,每个二进制位都对应一个偏移量(我们可以称之为一个索引或者位格)。
Bitmap支出的最大位数是2的32次方位,他可以极大的节约存储空间,使用512M内存就可以存储多达42.9亿的字节信息(2^32 = 4294967296)
能干嘛
用于状态统计:Y/N,类似AtomicBoolean
根据需求
用户是否登陆过Y、N,比如京东每日签到送京豆
电影、广告是否被点击播放过
钉钉打卡上下班,签到统计
。。。
真实案例
日活统计
连续签到打卡
最近一周的活跃用户
统计指定用户一年中的登录天数
某用户按照一年365天,哪几天登录过?哪几天没有登录?全年登录的天数共计多少?
签到领取京豆
需求说明:
签到日历仅展示当月签到数据
签到日历需展示最近连续签到天数
假设当前日期是20210618,且20210616未签到
若20210617已签到且0618未签到,则连续签到天数为1
若20210617已签到且0618已签到,则连续签到天数为2
连续签到天数越多,奖励越大
所有用户均可签到
截至2020年3月31日的12个月,京东年度活跃用户数3.87亿,同比增长24.8%,环比增长超2500万,此外,2020年3月移动端日均活跃用户数同比增长46%假设10%左右的用户参与签到,签到用户也高达3千万。。。。。。
传统方法,传统mysql方式
- 建表SQL:
CREATE TABLE user_sign
(
keyid BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
user_key VARCHAR(200),#京东用户ID
sign_date DATETIME,#签到日期(20210618)
sign_count INT #连续签到天数
)
INSERT INTO user_sign(user_key,sign_date,sign_count)
VALUES ('20210618-xxxx-xxxx-xxxx-xxxxxxxxxxxx','2020-06-18 15:11:12',1);...
- 困难和解决思路
方法正确但是难以落地实现,o(╥﹏╥)o。
签到用户量较小时这么设计能行,但京东这个体量的用户(估算3000W签到用户,一天一条数据,一个月就是9亿数据)
对于京东这样的体量,如果一条签到记录对应着当日用记录,那会很恐怖......
如何解决这个痛点?
1 一条签到记录对应一条记录,会占据越来越大的空间。
2 一个月最多31天,刚好我们的int类型是32位,那这样一个int类型就可以搞定一个月,32位大于31天,当天来了位是1没来就是0。
3 一条数据直接存储一个月的签到记录,不再是存储一天的签到记录。
redis方法
基于redis的bitmap实现日历签到 建表-按位-redis bitmap
说明:
在签到统计时,每个用户一天的签到用1个bit位就能表示,
一个月(假设是31天)的签到情况用31个bit位就可以,一年的签到也只需要用365个bit位,根本不用太复杂的集合类型
基本命令
setbit
Setbit key offset value
Setbit键 偏移位 只能是0/1
Bitmap的偏移量是从零开始算的
Getbit
Getbit key offset
Setbit和getbit案例说明
按照天
按照年:
按年去存储一个用户的签到情况,365 天只需要 365 / 8 ≈ 46 Byte,1000W 用户量一年也只需要 44 MB 就足够了。
假如是亿级的系统,每天使用1个1亿位的Bitmap约占12MB的内存(10^8/8/1024/1024),10天的Bitmap的内存开销约为120MB,内存压力不算太高。在实际使用时,最好对Bitmap设置过期时间,让Redis自动删除不再需要的签到记录以节省内存开销。
Bitmap的底层编码说明,get命令操作如何
实质是二进制的ascii编码对应
Redis 里用type命令看看bitmap实质是什么类型? -- string
设置命令:两个setbit命令对k1进行设置后,对应的二进制串就是0100 0001
二进制串就是0100 0001对应的10进制就是65
Strlen-统计字节数占用多少
不是字符串长度,而是占据几个字节,超过8位后自己按照8位一组一byte再扩容
Bitcount
全部键里含有1的有多少个
Bittop
Hyperloglog
名词
什么是UV
Unique visitor 独立访客,一般理解为客户端IP 需要去重考虑
什么是PV
Page view,页面访问量,不用去重
什么是DAU
Daily active user 日活跃用户量,登录或者使用了某个产品的用户数(去重复登录的用户)
常用于反应网站、互联网应用或者网络游戏的运营情况
什么是MAU
Monthly active User 月活跃用户量
看需求
统计某个网站的UV、统计摸个文章的UV
用户搜索网站关键词数量
统计用户每天搜索不同词条个数
是什么
去重复统计功能的技术估计算法-就是hyperloglog
基数
是一种数据集,去重复后的真实个数
基数统计
用于统计一个集合中不重复的元素个数,就是对集合去重复后剩余元素的计算
总结就是去重脱水后的真实数据
Hyperloglog如何做的?如何演化出来的?
基数统计就是hyperloglog
去重统计的方式
- HashSet
- Bitmap
如果数据显较大亿级统计,使用bitmaps同样会有这个问题。
bitmap是通过用位bit数组来表示各元素是否出现,每个元素对应一位,所需的总内存为N个bit。
基数计数则将每一个元素对应到bit数组中的其中一位,比如bit数组010010101(按照从零开始下标,有的就是1、4、6、8)。
新进入的元素只需要将已经有的bit数组和新加入的元素进行按位或计算就行。这个方式能大大减少内存占用且位操作迅速。
But,假设一个样本案例就是一亿个基数位值数据,一个样本就是一亿
如果要统计1亿个数据的基数位值,大约需要内存100000000/8/1024/1024约等于12M,内存减少占用的效果显著。
这样得到统计一个对象样本的基数值需要12M。
结论
样本元素越多内存消耗急剧增大,难以管控+各种慢,对于亿级数据不太合适
办法
概率算法:通过牺牲准确率来换取空间,对于不要求绝对准确率的场景下可以使用,因为概率算法不直接存储数据本身。
通过一定的概率统计方法预估基准数,同时保证误差在一定范围内,由于又不存储数据故此可以大大节约内存。
Hyperloglog就是一种概率算法的实现。
原理说明
只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不是具体内容。
有误差:非精确统计,牺牲准确率来换取空间,误差仅仅只有0.81%左右。
这个误差的来源:Redis new data structure: the HyperLogLog - <antirez> redis之父安蒂雷斯回答
经典面试题
- 为什么redis集群的最大槽数是16384?
Redis集群并没有使用一致性hash而是引入了哈希槽的概念。Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。但为什么哈希槽的数量是16384(2^14)个呢?
CRC16算法产生的hash值有16bit,该算法可以产生2^16=65536个值。
换句话说值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod65536,而选择mod16384?
why redis-cluster use 16384 slots? · Issue #2576 · redis/redis · GitHub
说明1:
正常的心跳数据包带有节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。
这意味着它们包含原始节点的插槽配置,该节点使用2k的空间和16k的插槽,但是会使用8k的空间(使用65k的插槽)。
同时,由于其他设计折衷,Redis集群不太可能扩展到1000个以上的主节点。
因此16k处于正确的范围内,以确保每个主机具有足够的插槽,最多可容纳1000个矩阵,但数量足够少,可以轻松地将插槽配置作为原始位图传播。请注意,在小型群集中,位图将难以压缩,因为当N较小时,位图将设置的slot / N位占设置位的很大百分比。
说明2:
(1)如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。
在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb
因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
(2)redis的集群主节点数量基本不可能超过1000个。
集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。
(3)槽位越小,节点少的情况下,压缩比高,容易传输
Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率slot...s
基本命令
案例实战:天猫网站首页亿级UV的redis统计方案
需求
- UV的统计需要去重,一个用户一天内的多次访问只能算作一次
- 淘宝、天猫首页的UV,平均每天是1~1.5亿左右
- 每天存1.5亿的IP,访问者来了后先去查是否存在,不存在加入
方案:
- 用mysql -- × 直接不考虑
- 用redis的hash结构存储
redis——hash = <<keyDay,<<ip,1>>
按照ipv4的结构来说明,每个ipv4的地址最多是15个字节(ip = "192.168.111.1",最多xxx.xxx.xxx.xxx)
某一天的1.5亿 * 15个字节= 2G,一个月60G,redis死定了。
- Hyperloglog
为什么是12Kb?
每个桶取6位,16384*6÷8 = 12kb,每个桶有6位,最大全部都是1,值就是63
GEO
简介
Redis在3.2版本之后增加了地理位置的处理
地球上的地理位置是使用二维的经纬度表示,经度范围 (-180, 180],纬度范围 (-90, 90],只要我们确定一个点的经纬度就可以名曲他在地球的位置。
例如滴滴打车,最直观的操作就是实时记录更新各个车的位置,
然后当我们要找车时,在数据库中查找距离我们(坐标x0,y0)附近r公里范围内部的车辆
使用如下SQL即可:
select taxi from position where x0-r << x << x0 + r and y0-r << y << y0+r
原理
核心思想就是将球体转换为平面,区块转换为一点
主要分为三步
将三维的地球变为二维的坐标
在将二维的坐标转换为一维的点块
最后将一维的点块转换为二进制再通过base32编码
难点
GEOHash核心原理解析:https://www.cnblogs.com/LBSer/p/3310455.html
地理知识说明
经纬度
https://baike.baidu.com/item/%E7%BB%8F%E7%BA%AC%E7%BA%BF/5596978?fr=aladdin
经度与纬度的合称组成一个坐标系统。又称为地理坐标系统,它是一种利用三度空间的球面来定义地球上的空间的球面坐标系统,能够标示地球上的任何一个位置。
经线和纬线
是人们为了在地球上确定位置和方向的,在地球仪和地图上画出来的,地面上并线。
和经线相垂直的线叫做纬线(纬线指示东西方向)。纬线是一条条长度不等的圆圈。最长的纬线就是赤道。
因为经线指示南北方向,所以经线又叫子午线。 国际上规定,把通过英国格林尼治天文台原址的经线叫做0°所以经线也叫本初子午线。在地球上经线指示南北方向,纬线指示东西方向。
东西半球分界线:东经160° 西经20°。
查询
命令
GEOADD 多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中
GEOPOS 从键里面返回所有给定位置元素的位置(经度和纬度)
GEODIST 返回两个给定位置之间的距离。
GEORADIUS 以给定的经纬度为中心, 返回与中心的距离不超过给定最大距离的所有位置元素。
GEORADIUSBYMEMBER 跟GEORADIUS类似
GEOHASH 返回一个或多个位置元素的 Geohash 表示
命令实操
如何获得某个地址的经纬度
http://api.map.baidu.com/lbsapi/getpoint/
https://jingweidu.bmcx.com/
GEOADD添加经纬度坐标
命令如下:
GEOADD city 116.403963 39.915119 "天安门" 116.403414 39.924091 "故宫" 116.024067 40.362639 "长城" (中文乱码需要处理)
GEOPOS返回经纬度
GEOHASH返回坐标的geohash表示
geohash算法生成的base32编码值
3维变2维变1维
GEODIST 两个位置之间距离
后面参数是距离单位:
m 米
km 千米
ft 英尺
mi 英里
GEORADIUS
以半径为中心,查找附近的XXX
georadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 withhash desc
WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
WITHCOORD: 将位置元素的经度和维度也一并返回。
WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大
COUNT 限定返回的记录数。