一.Redis的使用场景
一个key对应一个数据集合
- 登录信息:一天对应一系列用户或移动设备id
- 电商网站上商品的用户评论列表:一个商品对应了一系列的评论
- 用户在手机app上的签到打卡信息:一天对应一系列的签到记录
- 应用网站上网页访问信息:一个网站对应一系列的访问点击
对集合中的数据进行统计
- 新增用户数和第二天的留存用户数
- 电商网站的商品评论中需要统计评论列表的最新评论
- 签到打卡中,需要统计一月中连续打卡的用户数
- 网页访问记录中,需要统计独立访客(Unique Visitor,UV)量。
二.常用集合的统计模式
聚合统计,排序统计,二值状态统计和基数统计
聚合统计
聚合统计:统计多个集合元素的聚合结果。
交集统计:统计多个集合共有元素。
差集统计:两个集合相比,统计其中一个集合独有的元素。
并集统计:统计多个集合的所有元素。
场景1:统计手机APP每天新增用户和第二天留存用户数
实现方案:
用一个集合记录所有登录过APP的用户ID,同时,用另一个集合记录每一天登录过APP的用户ID,然后对两个集合做聚合统计。
1.记录所有登录过APP的用户ID
user:id,表示记录的是用户id
Set集合,里面是所有登录过APP用户ID
将这个Set成为累计用户Set
2.每天登录的用户ID(每日用户set):
user:id:20220211
用户id
统计每天新增用户,计算每日用户set和累计用户set的差集。
实现:如果手机APP在8月3日上线,将当天登录的用户记录到key为user:id:20200803的Set中。
8月3日新增用户是user:id:20200803这个set中用户就是当天新增用户集。
SUNIONSTORE user:id user:id user:id:20200803
计算累计用户 Set 和 user:id:20200803 Set 的并集结果,结果保存在 user:id 这个累计用户 Set 中。
SDIFFSTORE user:new user:id:20200804 user:id
将8月4日的登录用户ID记录到user:id:20200804的set中,求出累计用户 Set 和 user:id:20200804 Set 的差集,就是8月4日的新增用户数量。
计算8月4日留存用户:user:id:20200803和user:id:20200804两个set的交集
SINTERSTORE user:id:rem user:id:20200803 user:id:20200804
set聚合统计的风险
- 计算复杂度较高,在数据量较大情况下,会导致Redis实例阻塞。
解决方案
从库专门负责聚合计算,或把数据读取到客户端由客户端完成聚合统计,这样可以规避阻塞主库实例和其他从库实例的风险。
排序统计
集合元素排序需求方法实现
电商网站提供最新评论列表的场景
有序集合:集合中的元素可以按序排列。
Redis常用的四种集合类型(List,Hash,set,sorted Set)
List(元素进入顺序排序)和Sorted Sort(根据元素权重排序)属于有序集合。
排列。
List的实现方式
- 每个商品对应一个List评论集合,新的平均LPUSH命令插入List的队头。在只有一页评论时候,可以很清晰看到最新的评论。
- 分页实现时候可能会出现问题
List------> A,B,C,D,E,F
A是最新评论,F是最早评论
展示第一页3个评论时,用下面的命令,得到最新的三条评论A,B,C:
lrange product1 0 2
1."A"
2."B"
3."c"
再用下面的命令获取第二页的 3 个评论,也就是 D、E、F。
lrange product1 3 5
1."D"
2."E"
3."F"
问题
如果展示第二页前,新评论G被lpush命令插入评论list的队头,评论list变成{G,A,B,C,D,E,F},此时用刚才命令获取第二页评论时,会发现数据为
lrange product1 3 5
1."C"
2."D"
3."E"
产生原因:
lrange读取时就会读到旧数据。
(list会使数据下标变化,但是set中数据的score是不变的,分页时记录上一次的范围起始值就可以了 取第2页之前如果有新数据加入,那只会让逻辑上的第一页的数据量增大,第二页起始不变,因为记录了原先第一页末尾的score,是fixed的)
Sorted Set的实现方式
sorted set中。
ZRANGEBYSCORE命令按照权重排序后返回元素,即使集合中的元素频繁更新,Sorted set也能通过ZRANGEBYSCORE命令准确获取按序排列的数据。
越新的评论权重越大,目前最新评论的权重是N,我们执行命令时候,就可以获得最新的10条评论。
ZRANGEBYSCORE comments N-9 N
数据频繁更新或者分页显示,优先考虑Sorted Set。
二值状态统计
场景三:用户在手机app上的签到打卡信息:一天对应一系列的签到记录
二值状态统计(集合元素的取值就只有0和1两种)。
只用记录签到(1)和未签到(0),所以这是非常典型的二值状态。
用1个bit位表示,一个月(31天)只用31个bit位就可以,而一年的签到只需要用到365个bit位。
Bitmap
Redis提供的扩展数据结构
Bitmap本身用String类型作为底层数据结构实现的一种统计二值状态的数据类型。
二进制的字节数组。
- 一个元素的二值状态。可以把Bitmap看做是一个bit数组。
- offset对bit数组的某一个bit位进行读写。
- Bitmap偏移量从0开始,也就是offset最小值是0。
- 当使用SETBIT对一个bit位进行读写操作时,这个bit位会被设置为1。
- Bitmap还提供了BITCOUNT操作,来统计这个bit数组中所有为"1"的个数。
Bitmap实现签到统计
需求:统计ID 3000用户在2020年8月份的签到情况
存入:
SETBIT uid:sign:3000:202008 2 1
查询:
GETBIT uid:sign:3000:202008 2
统计用户在8月份的签到次数
BITCOUNT uid:sign:3000:202008
记录1亿个用户10天签到情况,统计这10天连续签到的用户总数
技术支持:BITOP
Bitmap支持用BITOP命令对多个Bitmap按位做"与"或"异或"操作,操作结果存入新的Bitmap中。
"与"操作:三个Bitmap bm1,bm2,bm3对bit位做"与"操作,结果保存到一个新的Bitmap中
解决方案:
每天日期作为key,每个key对应一个1亿位的Bitmap.
每一个bit位对应一个用户当天的签到情况
10天的Bitmap做"与"操作,得到的结果也是一个Bitmap。
BITCONT统计下Bitmap中1的个数,得到连续签到10天的用户总数。
内存开销:
1亿位的Bitmap,占用内存12MB (10^8bit)
120MB
优化:
设置过期时间,让Redis自动删除不需要签到记录,以节省内存开销。
总结:
统计数据的二值状态,例如商品有没有,用户在不在。它只用一个bit位表示0或1。在记录海量数据时,Bitmap能够有效的节省内存空间。
基数统计
统计一个集合中不重复元素个数。例如:统计网页UV。
网页UV统计有个独特的地方,就是需要去重,一个用户一天内多次访问只能算作一次。
Redis集合类型中,Set类型默认支持去重。
Set实现方式:
一个用户user1访问page1时:
SADD page1:uv user1
你需要统计 UV 时,可以直接用 SCARD 命令,这个命令会返回一个集合中的元素个数。
Hash实现方式
HSET page1:uv user1 1
当要统计 UV 时,我们可以用 HLEN 命令统计 Hash 集合中的所有元素个数。
缺点:消耗内存
HyperLogLog
当集合元素数量非常多时,他计算基数所需空间总是固定的,而且还很小。
每个HyperLogLog只需要花费12KB内存,可以计算接近2^64个元素的基数。
HyperLogLog 中添加新元素)把访问页面的每个用户都添加到 HyperLogLog 中。
PFADD page1:uv user1 user2 user3 user4 user5
PFCOUNT命令直接获得page1的UV值,这个命令的作用就是返回HyperLogLog的统计结果。
PFCOUNT page1:uv
缺点:HyperLogLog的统计规则是基于概率完成的,误差率是0.81%。使用HyperLogLog统计UV是100万,实际UV可能是101万,如果需要精确统计结果,最好还是继续用Set或Hash类型。
数据类型 | 聚合统计 | 排序统计 | 二值状态统计 | 基数统计 |
set | 支持交,并,差集计算 | 不支持 | 不支持 | 精确统计大数据量时,效率低内存开销大 |
sorted set | 支持交,并集计算 | 支持 | ||
hash | 不支持 | 不支持 | ||
List | 不支持 | 支持 | 不支持,元素没有去重 | |
Bitmap | 与或异或 | 不支持 | 支持,大数据量时,效率高省内存 | 精确统计,大数据量时内存开销大于HyperLogLog |
HyperLogLog | 不支持 | 不支持 | 不支持 | 概率统计,大数据量时,非常节约内存 |