当前位置: 首页 > article >正文

Redis高阶4-数据统计

数据统计

一:hyperloglog

​ Hyperloglog就是去重复统计的基数估计算法

​ Redis在2.8.9版本添加了 HyperLogLog 结构。
​ Redis HyperLogLog是用来做基数统计的算法,HyperLogLog的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
​ 在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同元素的基数。
这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
​ 但是,因为HyperLogL og只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog 不能像集合那样,返回输入的各个元素。

1.1 名词解释

  1. 什么是UV

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

  2. 什么是PV

    Page View,页面浏览量

  3. 什么是DAU

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

  4. 什么是MAU

    MonthIy Active User,月活跃用户量

1.2 需求

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

因为主要的目标高效、巨量地进行计数,所以对存储的数据的内容并不太关心。

也就是说它只能用于统计巨量数量,不太涉及具体的统计对象的内容和精准性。

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

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

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

1.3 原理

  1. 概率算法

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

    HyperLogLog就是一种概率算法的实现。

  2. 原理说明

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

    Hyperloglog提供不精确的去重计数方案,牺牲准确率来换取空间,误差仅仅只是0.81%左右.

    http://antirez.com/news/75

1.4 场景模拟

  1. 需求

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

  2. 方案

    用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死定了。o(╥﹏╥)o

    hyperloglog

    为什么是只需要花费12Kb?

    请添加图片描述

  3. 代码

    HyperLogLogService

    @Service
    @Slf4j
    public class HyperLogLogService
    {
        @Resource
        private RedisTemplate redisTemplate;
    
        /**
         * 模拟后台有用户点击首页,每个用户来自不同ip地址
         */
        @PostConstruct
        public void init()
        {
            log.info("------模拟后台有用户点击首页,每个用户来自不同ip地址");
            new Thread(() -> {
                String ip = null;
                for (int i = 1; i <=200; i++) {
                    Random r = new Random();
                    ip = r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256);
    
                    Long hll = redisTemplate.opsForHyperLogLog().add("hll", ip);
                    log.info("ip={},该ip地址访问首页的次数={}",ip,hll);
                    //暂停几秒钟线程
                    try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
                }
            },"t1").start();
        }
    
    }
    

    HyperLogLogController

    @Api(description = "淘宝亿级UV的Redis统计方案")
    @RestController
    @Slf4j
    public class HyperLogLogController
    {
        @Resource
        private RedisTemplate redisTemplate;
    
        @ApiOperation("获得IP去重后的首页访问量")
        @RequestMapping(value = "/uv",method = RequestMethod.GET)
        public long uv()
        {
            //pfcount
            return redisTemplate.opsForHyperLogLog().size("hll");
        }
    
    }
    
  4. 启动测试

二:GEO

2.1 场景模拟

  1. 需求

    • 附近的酒店
    • 附近的人
    • 一公里内的各种商店
  2. 代码实现

    GeoController

    @Api(tags = "美团地图位置附近的酒店推送GEO")
    @RestController
    @Slf4j
    public class GeoController
    {
        @Resource
        private GeoService geoService;
    
        @ApiOperation("添加坐标geoadd")
        @RequestMapping(value = "/geoadd",method = RequestMethod.GET)
        public String geoAdd()
        {
            return geoService.geoAdd();
        }
    
        @ApiOperation("获取经纬度坐标geopos")
        @RequestMapping(value = "/geopos",method = RequestMethod.GET)
        public Point position(String member)
        {
            return geoService.position(member);
        }
    
        @ApiOperation("获取经纬度生成的base32编码值geohash")
        @RequestMapping(value = "/geohash",method = RequestMethod.GET)
        public String hash(String member)
        {
            return geoService.hash(member);
        }
    
        @ApiOperation("获取两个给定位置之间的距离")
        @RequestMapping(value = "/geodist",method = RequestMethod.GET)
        public Distance distance(String member1, String member2)
        {
            return geoService.distance(member1,member2);
        }
    
        @ApiOperation("通过经度纬度查找北京王府井附近的")
        @RequestMapping(value = "/georadius",method = RequestMethod.GET)
        public GeoResults radiusByxy()
        {
            return geoService.radiusByxy();
        }
    
        @ApiOperation("通过地方查找附近,本例写死天安门作为地址")
        @RequestMapping(value = "/georadiusByMember",method = RequestMethod.GET)
        public GeoResults radiusByMember()
        {
            return geoService.radiusByMember();
        }
    
    }
    

    GeoService

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.geo.Distance;
    import org.springframework.data.geo.GeoResults;
    import org.springframework.data.geo.Metrics;
    import org.springframework.data.geo.Point;
    import org.springframework.data.geo.Circle;
    import org.springframework.data.redis.connection.RedisGeoCommands;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    
    @Service
    @Slf4j
    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));
    
            redisTemplate.opsForGeo().add(CITY,map);
    
            return map.toString();
        }
    
        public Point position(String member) {
            //获取经纬度坐标
            List<Point> list= this.redisTemplate.opsForGeo().position(CITY,member);
            return list.get(0);
        }
    
    
        public String hash(String member) {
            //geohash算法生成的base32编码值
            List<String> list= this.redisTemplate.opsForGeo().hash(CITY,member);
            return list.get(0);
        }
    
    
        public Distance distance(String member1, String member2) {
            //获取两个给定位置之间的距离
            Distance distance= this.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(50);
            //半径10公里内
            Distance distance=new Distance(10, Metrics.KILOMETERS);
            GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,member, distance,args);
            return geoResults;
        }
    }
    
  3. 启动测试

三 bitmap

请添加图片描述

说明:用String类型作为底层数据结构实现的一种统计二值状态的数据类型

位图本质是数组,它是基于String数据类型的按位的操作。该数组由多个二进制位组成,每个二进制位都对应一个偏移量(我们可以称之为一个索引或者位格)。Bitmap支持的最大位数是232位,它可以极大的节约存储空间,使用512M内存就可以存储多大42.9亿的字节信息(232 = 4294967296)

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

3.1 需求

​ 签到日历仅展示当月签到数据签到日历需展示最近连续签到天数假设当前日期是20210618,且20210616未签到若20210617已签到且0618未签到,则连续签到天数为1若20210617已签到且0618已签到,则连续签到天数为2连续签到天数越多,奖励越大所有用户均可签到截至2020年3月31日的12个月,京东年度活跃用户数3.87亿,同比增长24.8%,环比增长超2500万,此外,2020年3月移动端日均活跃用户数同比增长46%假设10%左右的用户参与签到,签到用户也高达3千万。。。。。。o(╥﹏╥)o

传统mysql方式

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);
**SELECT ** sign_count **FROM ** user_sign **WHERE ** user_key = **‘20210618-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;

方法正确但是难以落地实现,o(╥﹏╥)o。

签到用户量较小时这么设计能行,但京东这个体量的用户(估算3000W签到用户,一天一条数据,一个月就是9亿数据)

对于京东这样的体量,如果一条签到记录对应着当日用记录,那会很恐怖…

如何解决这个痛点?

1 一条签到记录对应一条记录,会占据越来越大的空间。

2 一个月最多31天,刚好我们的int类型是32位,那这样一个int类型就可以搞定一个月,32位大于31天,当天来了位是1没来就是0。

3 一条数据直接存储一个月的签到记录,不再是存储一天的签到记录。

基于Redis的Bitmap实现签到日历

在签到统计时,每个用户一天的签到用1个bit位就能表示,一个月(假设是31天)的签到情况用31个bit位就可以,一年的签到也只需要用365个bit位,根本不用太复杂的集合类型

3.2 场景模拟

见布隆过滤器


http://www.kler.cn/a/516777.html

相关文章:

  • docker环境搭建,docker拉取mysql,docker制作自定义C++镜像
  • 汽车定速巡航
  • 人脸识别打卡系统--基于QT(附源码)
  • 【Redis】持久化机制
  • 基于springboot社区医院管理系统
  • 实战经验:使用 Python 的 PyPDF 进行 PDF 操作
  • Go学习:iota枚举
  • React第二十四章(自定义hooks)
  • 利用 SAM2 模型探测卫星图像中的农田边界
  • 【CES2025】超越界限:ThinkAR推出8小时满电可用的超轻AR眼镜AiLens
  • Formality:时序变换(二)(不可读寄存器移除)
  • C# Interlocked 类使用详解
  • 深度学习|表示学习|卷积神经网络|局部链接是什么?|06
  • 【博客之星】2024年度总结
  • YOLO(You Only Look Once)--实时目标检测的革命性算法
  • 【ChatGPT】意义空间与语义运动定律 —— AI 世界的神秘法则
  • C# 与.NET 日志变革:JSON 让程序“开口说清话”
  • 使用Layout三行布局(SemiDesign)
  • 单片机-STM32 WIFI模块--ESP8266 (十二)
  • 后端开发基础——JavaWeb(根基,了解原理)浓缩
  • 关于av_get_channel_layout_nb_channels函数
  • Scrapy之一个item包含多级页面的处理方案
  • docker运行长期处于activating (start)
  • 【十年java搬砖路】oracle链接失败问题排查
  • 基于ollama,langchain,springboot从零搭建知识库四【设计通用rag系统】
  • 掌握Spring事务隔离级别,提升并发处理能力