05_案例落地实战bitmap、hyperloglog、GEO

本笔记来源于:尚硅谷Redis零基础到进阶,最强redis7教程,阳哥亲自带练(附redis面试题)
b站视频

文章来自:
https://github.com/Romantic-Lei/Learning-in-practice

面试题

  1. 抖音电商直播,主播介绍的商品有评论,1个商品对应了一系列的评论,排序+展现+取前10条记录

  2. 用户在手机APP上的签到打卡信息:1天对应一系列用户的签到记录,新浪微博、钉钉打卡签到,来没来如何统计

  3. 应用网站上的网页访问信息:一个网页对应一系列的访问点击,淘宝网首页,每天有多少人浏览首页

  4. 公司系统上线后,说一下UV、PV、DAU分别是多少?

记录对集合中的数据进行统计

在移动应用中,需要统计每天的新增用户数和第2天的留存用户数;

在电商网站的商品评论中,需要统计评论列表中的最新评论;

在签到打卡中,需要统计一个月内连续打卡的用户数;

在网页访问记录中,需要统计独立访客(Unique Visitor. uV)量。

痛点:

类似今日头条、抖音、淘宝这样的额用户访问级别都是亿级的,请问如何处理?

需求痛点

亿级数据的收集+清洗+统计+展现

一句话:存的进+去得快+多维度展现

真正有价值的是统计

统计的类型

亿级系统中,常见的四种统计

  • 聚合统计

    • 统计多个集合元素的聚合结果,就是前面讲解过的交差并等集合统计

    • 复习命令

    • 交差并集和聚合函数的应用

  • 排序统计

    抖音短视频最新评论留言的场景,请你设计一个展现列表。(考察数据结构和设计思路)

    设计案例和回答思路:

    以抖音vcr最新的留言评价为案例,所有评论需要两个功能,按照时间排序(正序、反序)+分页显示能够排序+分页显示的redis数据结构是什么合适?

    zset

    在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议使用ZSet

  • 二值统计

    集合元素的取值就只有0和1两种。在钉钉上签到打卡的场景中,我们只用记录有签到(1)或没有签单(0)

    见bitmap

  • 基数统计

    指统计一个集合中不重复的元素个数

    见HyperLogLog

HyperLogLog

名词、行话谈资

  • 什么是UV

    Unique Visitor,独立访客,一般理解为客户端IP

    需要去重考虑

  • 什么是PV

    Page View,页面浏览量

    不用去重

  • 什么是DAU

    Daily Active User,日活跃量用户,登录或者使用了某个产品的用户数(去重复登录的用户)

    常用于反映网站、互联网应用或者网络游戏的运营情况

  • 什么是MAU

    Monthly Active User,月活跃用户量

看需求

很多计数类场景,比如每日注册IP数、每日访问IP数、页面实时访问数PV、访问用户数UV等。

因为主要的目标高效、巨量地进行计数,所以对存储的数据的内容并不太关心。也就是说它只能用于统计巨量数量,不太涉及具体的统计对象的内容和精准性。

统计单日一个页面的访问量(PV),单次访问就算一次。

统计单日一个页面的用户访问量(UV),即按照用户为维度计算,单个用户一天内多次访问也只算一次。

多个key的合并统计,某个门户网站的所有模块的PV聚合统计就是整个网站的总PV。

是什么(基础篇写过)

  • 基数:是一种数据集,去重复后的真实个数

    案例

  • 取重复统计功能的基数估算算法-就是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

    如果统计1000个对象样本(1w个亿级),就需要117.1875G将近120G,可见使用bitmaps还是不适用大数据量下(亿级)的基数计数场景,但是bitmaps方法是精确计算的。

  • 结论

    样本元素越多内存消耗急剧增大,难以管控+各种慢,对于亿级统计不太合适,量变引起质变

  • 办法

    概率算法:

    通过牺牲准确率来换取空间,对于不要求绝对准确率的场景下可以使用,因为概率算法不直接存储数据本身,通过一定的概率统计方法预估基数值,同时保证误差在一定范围内,由于又不储存数据故此可以大大节约内存.
    HyperLogLog就是一种概率算法的实现。

原理说明

  • 只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不是具体内容

  • 有误差

    HyperLogLog提供不精确的去重计数方案

    只牺牲准确率来换取空间,误差仅仅只是0.81%左右

  • 这个误差率如何来的?

    http://antirez.com/news/75

    Redis之父安特雷兹回答:

首页亿级UV的Redis设计方案

需求

UV的统计需要去重,一个用户一天内的多次访问只能算作一次

淘宝、天猫首页的UV,平均每天是1~1.5个亿

每天存1.5个亿的IP,访问者来了后先去查是否存在,不存在就加入

方案讨论

  • 用MySQL,一个亿的数据,扛不住啊。。。高并发下,3000万的数据就需要分库分表了

  • 用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

GEO

Redis之GEO

面试题简介

命令复习

  • GEOADD 添加经纬度坐标

  • GEOPOS返回经纬度

  • GEOHASH返回坐标的geohash表示

    geohash算法生成的base32编码值,3维变2维变1维

  • GEODIST两个位置之间的距离

  • GEORADIUS

    以半径为中心,查找附近的XXX

  • GEORADIUSBYMEMBE

附近酒店的推送

需求分析

推送当前位置,指定范围内的酒店信息

架构设计

Redis的新类型GEO

http://www.redis.cn/commands/geoadd.html

编码实现

关键点:GEORADIUS,以给定的经纬度为中心,找出某一半径内的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public string geoAdd() {
Map<String,point> map = new HashMap<>();
map.put("天安门", new Point( x: 116.403963, y: 39.915119));
map.put("故宫", new point( x: 116.403414, y: 39.924091));
map.put("长城", new Point( x: 116.024067, y: 40.362639));
redisTemplate.opsForGeo().add(CITY,map);
return map.tostring();
}

public Point position(string member){
// 获取经纬度坐标,这里的member可以有多个,返回的list也可以有多个
list<Point> list = redisTemplate.opsForGeo().position(CITY, member);
return list.get(0);
}

public string hash(string member) {
//geohash算法生成的base32编码值,这里的member可以有多个,返回的list也可以有多个
List<string> list = redisTemplate.opsForGeo().hash(CITY,member);
return list.get(o);
}

public Distance distance( string member1,string member2) {
//获取两个给定位置之间的距离
Distance distance = redisTemplate.opsForGeo().distance(CITY,member1,member2,RedisGeoCommands.DistanceUnit.KILOMETERS);
return distance;
}

public GeoResults radiusByxy() {
//通过经度,纬度查找附近的,北京王府井位置116.418017, 39.914402
Circle circle = new Circle(116. 418017, 39 .914402, Metrics . KILOMETERS . getMultiplier());
//返@50条:
RedisGeoCommands.GeoRadiusCommandArgs args =
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults=
this.redisTemplate.opsForGeo().radius(CITY, circle, args);
return geoResults;
}

public GeoResults radiusByMember() {
//通过地方查找附近
String member="天安门";
//返回50条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates( ).sortAscending().limit(5e) ;
//半径10公里内
Distance distance=new Distance(10,Metrics.KILOMETERS);
GeoResults<RedisbeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForbeol().radius(CITY,member,distance,angs);
return geoResults;
}

Bitmap

大厂真实案例

日活统计

连续签到打卡

最近一周的活跃用户

统计指定用户一年之中的登录天数

某用户按照一年365天,哪几天登陆过,哪几天没有登录?

是什么

由0和1状态表现的二进制位的bit数组

能干嘛

用于状态统计,Y、N类似AtomicBoolean

看需求:

  • 用户是否登录过Y、N,比如京东每日签到送京豆

  • 电影、广告是否被点击播放过

  • 钉钉打卡上下班,签到统计

京东签到领取京豆

  • 需求说明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    签到日历仅展示当月签到数据
    签到日历需展示最近连续签到天数
    假设当前日期是20210618,且20210616未签到

    若20210617已签到且0618未签到,则连续签到天数为1

    若20210617已签到且0618已签到,则连续签到天数为2

    连续签到天数越多,奖励越大
    所有用户均可签到
    截至2020年3月31日的12个月,京东年度活跃用户数3.87亿,同比增长24.8%,环比增长超2500万,此外,2020年3月移动端日均活跃用户数同比增长46%假设10%左右的用户参与签到,签到用户也高达3千万。。。
  • 小厂方法,传统MySQL方式

    • 建表语句
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    CREATE TABLE user_sign
    keyid BIGINT NOT NULL PRIMARY KEY AUTO INCREMENT,
    user_key VARCHAR(200), #京东用户ID
    sign_date DATETIME,#签到日期(20220618)
    sign_count INT #连续签到天数

    INSERT INTO user_sign(user_key,sign_date,sign_count)
    VALUES ('28216618-XXXX-XXXX-XXXX-XXXXXXXXXXXX','2022-06-18 15:11:12',1);

    SELECT
    sign_count
    FROM
    user_sign
    WHERE
    user_key =20216618-XXXX-XXXX-XXXX-XXXXXXXXXXXX
    AND sign date BETWEEN '2020-06-17 00:00:00' AND '2020-06-18 23:59:59'
    ORDER BY
    sign_date DESC
    LIMIT 1;
    • 困难和解决思路

      方法正确但是难以落地实现。
      签到用户量较小时这么设计能行,但京东这个体量的用户(估算3000W签到用户,一天一条数据,一个月就是9亿数据)对于京东这样的体量,如果一条签到记录对应着当日签到记录,那会很恐怖……

      如何解决这个痛点?

      1. 一条签到记录对应一条记录,会占据越来越大的空间。
      2. 一个月最多31天,刚好我们的int类型是32位,那这样一个int类型就可以搞定一个月,32位大于31天,当天来了就是1没来就是0。
      3. 一条数据直接存储一个月的签到记录,不再是存储一天的签到记录。
  • 大厂方法,基于Redis的Bitmap实现签到日历

    建表-按位-redis-bitmap

    在签到统计时,每个用户一天的签到用1个bit位就能表示

    一个月(假设是31天)的签到情况用31个bit位就可以,一年的签到也只需要用365个bit位,根本不用太复杂的集合类型

命令复习

具体使用见基础篇

案例结合Bitmap类型签到+结合布隆过滤器实现


05_案例落地实战bitmap、hyperloglog、GEO
http://yuanql.top/2023/05/28/11_Redis/Redis_尚硅谷/Redis高级篇/05_案例落地实战bitmap、hyperloglog、GEO/
作者
Qingli Yuan
发布于
2023年5月28日
许可协议