Redis大数据统计

一. 相关面试题

1. 面试题一

  • 抖音电商直播,主播介绍商品有评论,1个商品对应一系列评论,排序+展示+取前10条记录?
  • 用户在手机APP上的签到打卡信息:1天对应一系列用户的签到记录,新浪微博、钉钉打卡签到,没来如何统计?
  • 应用网站傻姑娘的网页浏览信息:1个网页对应一系列的点击访问,淘宝网首页,每天有多少人浏览首页
  • 你们公司系统上线后,说一下UV、PV和DAU分别是多少

2. 面试题二

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

在移动应用中,需要统计每天的新增用户数和第二天的留存用户数
在电商网站的商品评论中,需要统计评论列表中的最新评论
在签到打卡中,需要统计一个月内连续打卡的用户数
在网页访问中,需要统计独立访客(UV)的量

痛点:

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

上面问题的关键点就是,对于亿级数据的收集、清洗、统计和展现,如何存、如何取得快,真正有价值的是如何统计

二. 统计的类型

亿级系统中常见的统计有四种:

1. 聚合统计

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

  • 集合的差集运算: A − B A-B AB

属于A但不属于B的元素构成的集合

SDIFF key [key...]
  • 集合的并集运算: A ∪ B A \cup B AB

属于A或者属于B的元素合并后的集合

SUNION key[key...]
  • 集合的交集运算: A ∩ B A \cap B AB
SINTER key [key...]
SINTERCARD numkeys key[key ...] [LIMIT limit]

属于A同时也属于B的集

2. 排序统计

例如:抖音段视频最新评论留言的场景,请你设计一个列表。考察你的数据结构和设计思路

对于上面的需求我们可以使用zset数据结构

在这里插入图片描述

3. 二值统计

集合元素的取值就只有0和1两种,在钉钉上班签到打卡的场景中,我们只需要用记录有签到或没签到,此时可以用redis的bitmap数据结构。

4. 基数统计

统计一个集合中不重复的元素个数,这里使用redis的hyperloglog数据结构。

三. Hyperloglog

1. 专业名词

  • UV

Unique Visitor,独立访客,一般理解为客户端IP(需要去重考虑)

  • PV

Page View,页面访问量(不用去重)

  • DAU

Daily Active User,日活跃用户量,登陆或者使用了某个产品的用户数(去除重复登陆的用户),常用于反映网站、互联网应用或者网络游戏运营情况

  • MAU

Mouthly Active User,月活跃用户量

很多计数类场景,比如每日注册IP数,每日访问IP数,页面实时访问数,访问用户数,因为主要的目标是高效、巨量的进行计数,所以对存储的数据内容并步关心。

2. Hyperloglog使用

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

去重复统计功能的基数估计算法就是HyperLogLog,它的优点是,在输入元素数量或者体积非常非常大的时候,计算基数的空间总是固定的,并且很小。在Redis里面,每个HyperLogLog只需要12kb的内存,就可以计算洁净2^64个的不同元素的基数,但是HyperLogLog只会根据输入元素来计算基数,而不会存储输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。

添加元素到 HyperLogLog:

PFADD key element [element ...]
PFADD my_hyperloglog a b c d e f g

获取 HyperLogLog 的基数估计值:

PFCOUNT key [key ...]
PFCOUNT my_hyperloglog

合并多个 HyperLogLog:

PFMERGE destkey sourcekey [sourcekey ...]

> PFADD hll1 a b c d
(integer) 1
> PFADD hll2 c d e f
(integer) 1
> PFMERGE hll_union hll1 hll2
OK
> PFCOUNT hll_union
(integer) 6

3. Hyperloglog原理

去重的思路:

  • hashset:在java中hashset就是一个无重复元素的集合(但是数据量很大不适合)
  • bitmap:

bitmap是通过用位bit数组来表示各个元素是否出现,每个元素队员一位,所需的总内存是N个bit。基数技术则嫁给你每一个元素对应到bit数组中的其中一位,新进入的元素只需要讲已经有的bit数组和新加入的元素进行按位或计算就行,这个方式能大大减少内存占用,且操作迅速。例如,假设一个样本案例就是一亿个基数位值数据,一个样本就是一亿,如果要统计1亿个数据的基数位值,大约需要内存1000000000/8/1024约为12M,内存减少占用的效果显著,这样得到的统计一个对象样本的基数值就是12M。但是,统计10000个对象样本(1w个亿级),就需要117.1875G,可见使用bitmaps还是不适合大数据量下(亿级)的基数计数场景。
但是bitmap的统计是精确的不会有误差

  • 概率算法:

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

HyperLogLog就是一种概率算法的体现

HyperLogLog只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不体现具体内容。它提供一种不精确的去重计数方案,误差大概在0.81%左右。

在这里插入图片描述
在这里插入图片描述

4. Hyperloglog案例

  • 需求

UV的统计需要去重,一个用户一天内的多次访问只能算做一次,淘宝、天猫首页的UV,平均每天是1~1.5亿左右。

  • 案例实现

Service

@Service
public class HyperLogLogService {
    @Autowired
    private RedisTemplate redisTemplate;

    @PostConstruct
    public void initIp() {
        new Thread(() -> {
            String ip = null;
            for (int i = 0; i < 200; i++) {
                Random random = new Random();
                ip = random.nextInt(256) + "." + random.nextInt(256) + "." + random.nextInt(256) + "." + random.nextInt(256);
                redisTemplate.opsForHyperLogLog().add("hll", ip);
                System.out.println("ip={" + ip + "}");
                try{
                    TimeUnit.SECONDS.sleep(3);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }, "t1").start();
    }
    public long UV(){
        return redisTemplate.opsForHyperLogLog().size("hll");
    }
}

Controller

@RestController
public class HyperLogLogController {
    @Autowired
    HyperLogLogService hyperLogLogService;

    @RequestMapping(value = "/uv",method = RequestMethod.GET)
    public long UV() {
        return hyperLogLogService.UV();
    }
}

统计结果
在这里插入图片描述

四. GEO

1. 面试题

移动互联网时代LBS应用越来越多,交友软件中附近的小姐姐,外卖软件中附近的美食店,打车软件附近的车辆等等,这种位置选择是如何实现的?

为什么不使用Mysql?

  • 查询性能问题,如果并发高,数据量大这种查询是会搞垮Mysql数据库的
  • 一般mysql查询是一个平面矩阵访问,而叫车服务是要以我为中心N公里为半径的圆形覆盖
  • 精确的问题,我们知道地球不是平面,而是一个球,这种矩形计算在长距离计算时会有很大误差

2. GEO使用

GEOADD 添加经纬度坐标

GEOADD key longitude latitude member [longitude latitude member ...]

> GEOADD cities 13.361389 38.115556 "Athens" 15.087269 37.502669 "Thessaloniki"
(integer) 2

GEO返回经纬度

GEORADIUS key longitude latitude radius m|km|mi|ft [WITHCOORD]

> GEORADIUS cities 15 37 200 km WITHCOORD
1) 1) "Thessaloniki"
   2) 1) "22.942600429058075"
      2) "40.640062264928304"

GEOHASH返回坐标的geohash表示

Geohash算法生成base32编码值,3维变为2维,2维变一维

GEOHASH key member [member ...]

> GEOADD cities 13.361389 38.115556 "Athens" 15.087269 37.502669 "Thessaloniki"
(integer) 2
> GEOHASH cities "Athens"
1) "sqdtr74hyu0"

GEODIST两个位置之间的距离

GEODIST key member1 member2 [unit]

> GEODIST cities "Athens" "Thessaloniki" km
"303.6469"

获取指定位置范围内的地理位置信息

GEORADIUS key longitude latitude radius m|km|mi|ft [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

> GEORADIUS cities 15 37 200 km
1) "Thessaloniki"

获取指定位置范围内的地理位置信息并按距离排序:

GEORADIUSBYMEMBER key member radius m|km|mi|ft [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

> GEORADIUSBYMEMBER cities "Athens" 200 km
1) "Athens"

3. GEO案例

  • 需求

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

  • 案例实现

Controller

@RestController
public class GeoController {
    @Autowired
    private GeoService geoService;

    @RequestMapping(value = "/geoadd", method = RequestMethod.GET)
    public String geoAdd() {
        return geoService.geoAdd();
    }

    @RequestMapping(value = "/geopos", method = RequestMethod.GET)
    public Point position(String member) {
        return geoService.postion(member);
    }

    @RequestMapping(value = "/geohash", method = RequestMethod.GET)
    public String hash(String member) {
        return geoService.hash(member);
    }

    @RequestMapping(value = "/geodist", method = RequestMethod.GET)
    public Distance distance(String member1, String member2) {
        return geoService.distance(member1, member2);
    }

    @RequestMapping(value = "/georadius", method = RequestMethod.GET)
    public GeoResults radiusByxy() {
        return geoService.radiusByxy();
    }

    @RequestMapping(value = "/georadiusByMerber", method = RequestMethod.GET)
    public GeoResults radiusByMember() {
        return geoService.radiusByMember();
    }
}

Service

@Service
public class GeoService {

    public static final String CITY = "city";

    @Autowired
    private RedisTemplate redisTemplate;

    public String geoAdd() {
        Map<String, Point> map = new HashMap<>();
        map.put("天安门", new Point(116.403963, 39.915119));
        map.put("故宫", new Point(116.403414, 39.924091));
        map.put("长城", new Point(116.024067, 40.362639));
        map.put("北京大学", new Point(116.316833,39.998877));
        map.put("清华大学", new Point(116.333374,40.009645));
        redisTemplate.opsForGeo().add(CITY, map);
        return map.toString();
    }

    public Point postion(String member) {
        List<Point> position = redisTemplate.opsForGeo().position(CITY, member);
        return position.get(0);
    }

    public String hash(String member) {
        List<String> hash = redisTemplate.opsForGeo().hash(CITY, member);
        return hash.get(0);
    }

    public Distance distance(String member1, String member2) {
        Distance distance = redisTemplate.opsForGeo().distance(CITY, member1, member2, RedisGeoCommands.DistanceUnit.KILOMETERS);
        return distance;
    }

    public GeoResults radiusByxy() {
        //116.418038,39.919790
        Circle circle = new Circle(116.418038, 39.919790, Metrics.KILOMETERS.getMultiplier());
        RedisGeoCommands.GeoRadiusCommandArgs geoRadiusCommandArgs = RedisGeoCommands
                .GeoRadiusCommandArgs
                .newGeoRadiusArgs()
                .includeDistance()
                .includeCoordinates()
                .sortDescending();
        GeoResults<RedisGeoCommands.GeoLocation<String>> radius = redisTemplate.opsForGeo().radius(CITY, circle, geoRadiusCommandArgs);
        return radius;
    }

    public GeoResults radiusByMember() {
        Circle circle = new Circle(116.418038,39.919790,Metrics.KILOMETERS.getMultiplier());
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
                .newGeoRadiusArgs()
                .includeDistance()
                .includeCoordinates()
                .sortDescending();
        GeoResults<RedisGeoCommands.GeoLocation<String>> tian = redisTemplate.opsForGeo().radius(CITY, "天安门", new Distance(50), args);
        return tian;
    }
}

测试结果
在这里插入图片描述
在这里插入图片描述

五. BitMap

1. 面试题

网站日活统计,连续签到打卡,最近一周日活统计,统计指定用户一年之内的登陆天数,某用户按照一年365天,哪天登录过,哪几天没登陆,全年中的登陆天数。

2. BitMap使用

在这里插入图片描述

  • 设置位
SETBIT key offset value

key: 位图的键名。
offset: 位图中的偏移量(从0开始)。
value: 要设置的值,可以是 0 或 1。

  • 获取位
GETBIT key offset

  • 统计位
BITCOUNT key [start end]
# 使用 BITCOUNT 命令统计位图中值为 1 的位的数量。
  • 位运算
BITOP operation destkey key [key ...]

operation: 要执行的位运算,可以是 AND、OR、XOR、NOT 中的一种。
destkey: 结果存放的键名。
key [key …]: 参与运算的位图键名。

bitmap的案例后面结合布隆过滤器来讲解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值