0
点赞
收藏
分享

微信扫一扫

Redis中bitmap、hyperloglog和GEO的实际应用

一、生产环境下需要解决的问题

在生产环境中常常无论是面试还是实际工作都会遇到如下问题:

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

上面的问题主要是当数据量比较大的时候,如有上亿条数据的时候,数据的收集、统计和展示就是一个难点,怎么样存储、读取和分析统计有价值的数据就变得尤为困难

二、常见的统计类型有哪些?

常见的统计类型有一下四种如下:

2.1.聚合统计

统计多个集合元素的聚合结果(交并差集合统计)set集合:

Redis中bitmap、hyperloglog和GEO的实际应用_数据

2.2.排序统计

像美团最新评论留言的场景,设计一个展现列表,在对需要展示最新列表、排行榜(可以安装最新时间、低分、晒图)等场景展示时,建议使用zset

Redis中bitmap、hyperloglog和GEO的实际应用_redis_02

2.3.二值统计

集合元素的取值就只有 0 和 1 ,来记录签到还是没签到 bitmap

2.4.基数统计

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

三、hyperloglog

3.1.常见访问量名词

  • UV (Unique Visitor) 独立访客,一般理解为客户端IP (需要考虑去重);
  • PV (Page View) 页面浏览量,不用去重;
  • DAU(Daily Active User)日活跃用户量,登录或者使用了某个产品的用户数(去重复登录的用户),常用于反映网站、互联网应用或者网络游戏的运营情况;
  • MAU(Monthly Active User)月活跃用户量;

很多计数类场景,例如:每日注册 IP 数、每日访问 IP 数、页面实时访问数 PV、访问用户数 UV等。因为主要的目标高效、海量的数据进行计数,所以对存储的数据的内容并不太关心。也就是说它只能用于统计海量数量,不太涉及具体的统计对象的内容和精准性。

  • 统计单日一个页面的访问量(PV),单次访问就算一次。
  • 统计单日一个页面的用户访问量(UV),即按照用户为维度计算,单个用户一天内多次访问也只算一次。
  • 多个key的合并统计,某个门户网站的所有模块的PV聚合统计就是整个网站的总PV。

3.2.hyperloglog

Redis 2.8.9 版本中新增了 HyperLogLog 类型。HyperLoglog 是 Redis 适用于海量数据的计算、统计,其特点是占用空间小,计算速度快。

HyperLoglog 采用了一种基数估计算法,因此,最终得到的结果会存在一定范围的误差(标准误差为 0.81%)。每个 HyperLogLog key 只占用 12 KB 内存,所以理论上可以存储大约2^64个值,而 set(集合)则是元素越多占用的内存就越多,两者形成了鲜明的对比 。

基数定义

基数定义:一个集合中不重复的元素个数就表示该集合的基数,比如集合 {1,2,3,4,1,2} ,它的基数集合为 {1,2,3,4} ,所以基数为 4。HyperLogLog 正是通过基数估计算法来统计输入元素的基数。

场景应用

HyperLogLog 也有一些特定的使用场景,它最典型的应用场景就是统计网站用户月活量,或者网站页面的 UV(网站独立访客)数据等。UV 与 PV(页面浏览量) 不同,UV 需要去重,同一个用户一天之内的多次访问只能计数一次。这就要求用户的每一次访问都要带上自身的用户 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。当一个网站拥有巨大的用户访问量时,我们可以使用 Redis 的 HyperLogLog 来统计网站的 UV (网站独立访客)数据,它提供的去重计数方案,虽说不精确,但 0.81% 的误差足以满足 UV 统计的需求。

3.3.工作原理

hyperloglog就是基数统计,可以直接去重,说到去重复统计功能有一下方式:

  • hashSet
  • bitmap
  • bitmap是通过用位bit数组来表示各元素是否出现,每个元素对应一位,所需的总内存为N个bit。
  • 新进入的元素只需要将已经有的bit数组和新加入的元素进行按位或计算就行。这个方式能大大减少内存占用且位操作迅速。
  • 如果数据较大,比如一个样本案例就是一亿个基数拉值数据,一个样本就是一亿,如果要统计一亿个数据的基数拉值,大约需要内存 1100000000/8/1024/1024 约等于 12M,内存减少占用的效果显著。这样得到统计一个对象样本的基数值需要12M。
  • 如果统计10000个对象样本(1w个亿级),就需要117.1875G将近120G,可见使用bitmaps还是不适用大数据量下(亿级)的基数计数场景,
  • 但是bitmap是精确计算的

上面的无论是hashSet或者bitmap,当数据量比较大的时候,就会量变会引起质变,所以以上两种方式均不能满足需要,解决方案:

  • 通过牺牲准确率来换取空间,对于不要求绝对准确率的场景下可以使用,因为概率算法不直接存储数据本身,
  • 通过一定的概率统计方法预估基数值,同时保证误差在一定范围内,由于又不储存数据故此可以大大节约内存。
  • 而HyperLogLog就是一种概率算法的实现,我们通过HyperLogLog就可以实现网站用户月活量,或者网站页面的 UV(网站独立访客)数据等统计。

hyperloglog原理说明

  • 只是进行不重复的基数统计,不是集合也不会保存数据,只记录数量而记录具体的数据内容
  • 但是hyperloglog统计存在误差
  • hyperloglog提供不精确的去重技术方案
  • 牺牲准确率来换取空间,误差仅仅只是 0.81% 左右)
  • 关于误差在redis之父安特雷兹的博客中有所说明:http://antirez.com/news/75,如下图:

Redis中bitmap、hyperloglog和GEO的实际应用_数据_03

3.3.亿级网站uv的redis统计方案实施

3.3.1.对于技术的选型

统计uv对于亿级访问量的网站而言可以有一下三种方式:

  1. 使用mysql:使用mysql如果并发量太高,数据都需要存入mysql中,导致mysql的检索也会变慢;
  2. 用redis的hash结构存储:按照ipv4的结构来说明,一个ip最多15个字节(ip=“192.168.238.1xx”),某一天 1.5亿*15个字节 = 2G,一个月60G,内存直接没了,加内存条也很难避免内存爆掉的问题;
  3. hyperloglog(推荐):在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2的64次方个不同元素的基数

3.3.2.为什么选择hyperloglog

Redis使用了2的14次方=16384个寄存器,按照上面的标准差,误差为0.81%,精度相当高。Redis使用一个long型哈希值的前14个比特用来确定桶编号,剩下的50个比特用来做基数估计。而2的6次方=64,所以只需要用6个比特表示下标值,在一般情况下,一个HLL数据结构占用内存的大小为16384*6 /8= 12kB,Redis将这种情况称为密集(dense)存储。

3.3.3.创建一个springboot项目

这里就是一个普通的springboot项目,在pom.xml中添加如下依赖:

<properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <jedis.version>4.3.1</jedis.version>
        <lettuce-core.version>6.2.3.RELEASE</lettuce-core.version>
        <junit.version>4.12</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.16.18</lombok.version>
        <swagger.version>2.9.2</swagger.version>
    </properties>

    <dependencies>
        <!--springboot通用依赖模块-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--SpringBoot与Redis整合依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${swagger.version}</version>
        </dependency>

        <!--通用基础配置-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
    </dependencies>

3.3.4.设置配置文件信息

在resources中添加如下内容:

server.port=7777
spring.application.name=redis7_study

# ========================logging=====================
logging.level.root=info
logging.level.com.atguigu.redis7=info
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n 

logging.file.name=D:/mylogs2023/redis7_study.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n

# ========================swagger=====================
spring.swagger2.enabled=true
#在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常,
#原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser,
# 导致出错,解决办法是matching-strategy切换回之前ant_path_matcher
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

# ========================redis单机=====================
spring.redis.database=0
# 修改为自己真实IP
spring.redis.host=192.168.42.132
spring.redis.port=6379
spring.redis.password=123456
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

3.3.4.创建config层

在下面创建 RedisConfig 内容如下:

package com.redis7.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    /**
     * redis序列化的工具配置类,下面这个请一定开启配置
     * 127.0.0.1:6379> keys *
     * 1) "ord102"  序列化过
     * 2) "\xac\xed\x00\x05t\x00\aord102"   没有序列化过
     * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法
     * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法
     * this.redisTemplate.opsForSet(); //提供了操作set的所有方法
     * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法
     * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法
     * @param lettuceConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();

        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}

创建 SwaggerConfig 内容如下:

package com.redis7.demo.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Configuration
@EnableSwagger2 //开启swagger功能
public class SwaggerConfig{
    @Value("${spring.swagger2.enabled}")
    private Boolean enabled;

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(enabled)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.redis7.demo")) //你自己的package
                .paths(PathSelectors.any())
                .build();
    }
    public ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
                .description("springboot项目整合")
                .version("1.0")
                .termsOfServiceUrl("https://www.augus.com/")
                .build();
    }
}

3.3.5.创建service层

在下面创建 HyperLogLogService 内容如下:

package com.redis7.demo.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * @author Augus
 */
@Service
@Slf4j
public class HyperLogLogService {
    @Resource
    private RedisTemplate redisTemplate;

    @PostConstruct //java自带的注解,在方法上加如该注解会在项目启动的时候执行该方法
    public void init(){
        log.info("--------------模拟后台有用户点击首页,每个用户来自不同ip地址-----------");
        new Thread(()->{
            String ip = null;
            for (int i=1; i<=200; i++){
                Random random = new Random();
                ip = random.nextInt(256) + "." + random.nextInt(256) + "." + random.nextInt(256) + "." + random.nextInt(256);

                //将ip存储到redis HyperLogLog数据中
                Long uvip = redisTemplate.opsForHyperLogLog().add("uvip", ip);

                log.info("ip:{},该ip地址访问首页的次数={}",ip,uvip);
                //模拟间隔时间
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
    }
}

3.3.6.创建controller层

在下面创建 HyperLogLogController 内容如下:

package com.redis7.demo.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@Api(tags = "亿级UV时Redis统计方案")
@RestController
public class HyperLogLogController {
    @Resource
    private RedisTemplate redisTemplate;

    @ApiOperation("获得IP去重后的首页访问量")
    @RequestMapping(value = "/getuv", method = RequestMethod.GET)
    public long GetUV() {
        //统计HyperLogLog()中uvip这个键所对应的值的个数
        return redisTemplate.opsForHyperLogLog().size("uvip");
    }
}

3.3.6.测试

先启动redis,然后启动项目,在浏览器访问 http://localhost:7777/swagger-ui.html#  这个是swagger的地址,然后调用接口,如下图即可看到统计的数据

Redis中bitmap、hyperloglog和GEO的实际应用_redis_04

 查看redis中数据和上面通过接口查询的数据量一致

Redis中bitmap、hyperloglog和GEO的实际应用_spring_05

四、GEO

4.1.常见问题说明

面试题说明:

移动互联网时代软件中位置信息越来越多,像交友软件中附近人、外卖软件中附近的美食店铺、打车软件附近的车辆等等。那这种附近各种形形色色的地理位置选择是如何实现的?存在那些问题:

  • 查询性能问题,如果并发高,数据量大这种查询是会导致mysql数据库宕机
  • 一般mysql查询的是一个平面矩形访问,例如叫车服务要以我为中心N公里为半径的圆形覆盖。
  • 精准度的问题,我们知道地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差,mysql就显得不合适
  • 那么地理位置的问题就可以通过Redis GEO来解决

如果想要获取经纬度的话,可以去百度地图拾取坐标系统 (baidu.com)

4.2.常用命令说明

1.GEOADD添加经纬度坐标

添加一个GEO地理位置代码如下:注意这里有中文,防止乱码在登录redis客户端的时候添加 --raw 参数

GEOADD city 108.953509  34.265619 "钟楼" 108.970604  34.224485 "大雁塔"  108.97638  34.270923  "永兴坊"

执行如下图:

Redis中bitmap、hyperloglog和GEO的实际应用_spring_06

2.GEOPOS返回经纬度

查看经纬度,例如:

GEOPOS city  永兴坊   钟楼

执行后如下:

Redis中bitmap、hyperloglog和GEO的实际应用_数据_07

3.GEOHASH返回坐标的geohash表示

返回坐标的geohash值:

GEOHASH city  永兴坊   钟楼  大雁塔

执行后如下:

Redis中bitmap、hyperloglog和GEO的实际应用_redis_08

4.GEODIST两个位置之间距离

查看两点之间的距离:

GEODIST city 永兴坊 钟楼 km

后面参数是距离单位:

  • m 米
  • km 千米
  • ft 英尺
  • mi 英里

5.GEORADIUS 以半径为中心,查找附近的位置坐标

georadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

GEORADIUS city 108.990011 34.260013 10 km withdist withcoord count 10 withhash desc

参数说明:

  • WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
  • WITHCOORD: 将位置元素的经度和维度也一并返回。
  • WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大
  • COUNT 限定返回的记录数。

Redis中bitmap、hyperloglog和GEO的实际应用_数据_09

6.GEORADIUSBYMEMBER 找出位于指定范围内的元素,中心点是由给定的位置元素决定

以钟楼和中心查找10km范围以内的位置坐标

GEORADIUSBYMEMBER city 钟楼 10 km withdist withcoord count 10 withhash

如下图:

Redis中bitmap、hyperloglog和GEO的实际应用_redis_10

4.3.以景点附近范围内的推送功能为案例演示

4.3.1.案例场景:

  • 美团app附近的酒店
  • 社交类app查找附近人
  • 高德地图附近的人或者一公里以内的各种营业厅、加油站、理发店、超市.....

4.3.2.架构设计

以Redis中GEO数据类型实现上述案例,参考地址:http://www.redis.cn/commands/geoadd.html,核心是:

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

4.3.3.案例落地

在service包下创建  GeoService,内容如下:

package com.redis7.demo.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.*;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.awt.*;
import java.util.HashMap;
import java.util.List;

/**
 * @Auther: Augus
 * @Date: 2023-04-03 20:49
 * @Description: com.redis7.demo.service
 * @version: 1.0
 */
@Service
@Slf4j
public class GeoService {
    public static final String CITY ="city";

    @Autowired
    private RedisTemplate redisTemplate;

    public String geoAdd(){
        //Point里面存放的经纬度
        HashMap<String, Point> stringPointHashMap = new HashMap<>();
        //给hashmap中添加值
        stringPointHashMap.put("兴庆公园", new Point(108.990011,34.260013));
        stringPointHashMap.put("钟楼", new Point(108.953509,34.265619));
        stringPointHashMap.put("永兴坊", new Point(108.97638,34.270923));
        stringPointHashMap.put("大雁塔", new Point(108.970604,34.224485));

        //写入到redis
        redisTemplate.opsForGeo().add(CITY,stringPointHashMap);
        return stringPointHashMap.toString();
    }

    public Point position(String member){
        //获取经纬度坐标
        List<Point> list = redisTemplate.opsForGeo().position(CITY,member);
        return list.get(0);
    }

    public String hash(String member){
        //geohash 算法生成的base32编码值
        List<String> list = redisTemplate.opsForGeo().hash(CITY, member);
        return list.get(0);
    }

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

    public GeoResults redisByXY(){
        //通过经度,纬度查找附件的,西安钟楼的经纬度108.953509,34.265619
        Circle circle = new Circle(108.953509, 34.265619, Metrics.KILOMETERS.getMultiplier());
        //返回符合要求的50条
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);

        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= 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= redisTemplate.opsForGeo().radius(CITY,member, distance,args);
        return geoResults;
    }
}

在 controller 包下创建  GeoService,内容如下:

package com.redis7.demo.controller;

import com.redis7.demo.service.GeoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @Auther: Augus
 * @Date: 2023-04-03 21:31
 * @Description: com.redis7.demo.controller
 * @version: 1.0
 */
@Api(tags = "地图位置附近的酒店推送GEO")
@RestController
@Slf4j
public class GeoController {
    @Resource
    private GeoService geoService;

    @ApiOperation("添加坐标geoadd")
    @RequestMapping(value = "/geoadd", method = RequestMethod.GET)
    public String getAdd(){
        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 rediusByXY(){
        return geoService.redisByXY();
    }

    @ApiOperation("通过地点名称查找的位置,这里以钟楼为例")
    @RequestMapping(value = "/georadiusByMember", method = RequestMethod.GET)
    public GeoResults radiusByMember(){
        return geoService.radiusByMember();
    }
}

重启项目,访问swagger,http://localhost:7777/swagger-ui.html#,如下分别测试:

  • 添加坐标geoadd:

Redis中bitmap、hyperloglog和GEO的实际应用_spring_11

  • 获取两个给定位置之间的距离

Redis中bitmap、hyperloglog和GEO的实际应用_spring_12

  • 获取经纬度生成的base32编码值geohash

Redis中bitmap、hyperloglog和GEO的实际应用_spring_13

  • 获取经纬度坐标geopos

Redis中bitmap、hyperloglog和GEO的实际应用_redis_14

  • 通过经纬度查找钟楼附件的位置

 

Redis中bitmap、hyperloglog和GEO的实际应用_redis_15

  • 通过地方查找附近,本例以钟楼作为地址演示

Redis中bitmap、hyperloglog和GEO的实际应用_redis_16

五、bitmap

5.1.常见场景说明

无论是在实际工作还是面试都会遇到一下问题:

  • 日活跃用户量的统计
  • 连续签到打卡
  • 最近—周的活跃用户
  • 统计指定用户━年之中的登陆天数
  • 某用户按照一年365天,哪几天登陆过?哪几天没有登陆?全年中登录的天数共计多少?

5.2.bitmap是什么?

Redis中bitmap、hyperloglog和GEO的实际应用_spring_17

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

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

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

5.3.bitmap能做什么?

位图用户状态的统计的场景,例如:

  • 京东每日签到送京豆电影
  • 开屏广告是否被点击播放过
  • 钉钉打卡上下班,签到统计

5.4.以京东每日签到领取京豆为案例说明

1.需求说明

签到日历仅展示当月签到数据,签到日历需展示最近连续签到天数,假设当前日期是20230318,且20230316未签到,那么显示如下:

  • 若20230317已签到且0318未签到,则连续签到天数为1
  • 若20230317已签到且0618已签到,则连续签到天数为2

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

Redis中bitmap、hyperloglog和GEO的实际应用_spring_18

 

 

 

Redis中bitmap、hyperloglog和GEO的实际应用_spring_19

2.传统实现上述功能方式

建表SQL:

CREATE TABLE user_sign
(
  keyid BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
  user_key VARCHAR(200),#京东用户ID
  sign_date DATETIME,#签到日期(20230318)
  sign_count INT #连续签到天数
)

插入数据:

INSERT INTO user_sign(user_key,sign_date,sign_count)
VALUES ('20230318-xxxx-xxxx-xxxx-xxxxxxxxxxxx','2023-03-18 18:11:12',1);

上述通过SQL存储每一条签到记录的方法正确但是难以落地实现,签到用户量较小时可以这样设计,但对于京东这个体量的用户(估算3000W签到用户,一天一条数据,一个月就是9亿数据),如果一条签到记录对应着当日用记录,那会很恐怖,如何解决这个痛点呢?,思路如下:

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

3.目前的解决方式可以通过redis的bitmap来实现签到功能

在签到统计时,每个用户一天的签到用1个bit位就能表示,一个月(假设是31天)的签到情况用31个bit位就可以,一年的签到也只需要用365个bit位,使用bitmap即可轻松解决。

4.基本命令

  • SETBIT key offset value :将第offset的值设为value value只能是0或1 offset 从0开始

127.0.0.1:6379> SETBIT k1 1 1
(integer) 0
127.0.0.1:6379> SETBIT k1 7 1
(integer) 0
127.0.0.1:6379> get k1
"A"

  • GETBIT key offset :获得第offset位的值

127.0.0.1:6379> GETBIT k1 1
(integer) 1
127.0.0.1:6379> GETBIT k1 2
(integer) 0
127.0.0.1:6379> GETBIT k1 7
(integer) 1
127.0.0.1:6379>

  • setbit和getbit案例说明

按照天:

Redis中bitmap、hyperloglog和GEO的实际应用_数据_20

按照年:

按年去存储一个用户的签到情况,365 天只需要 365 / 8 ≈ 46 Byte,1000W 用户量一年也只需要 44 MB 就足够了。

假如是亿级的系统,每天使用1个1亿位的Bitmap约占12MB的内存(10^8/8/1024/1024),10天的Bitmap的内存开销约为120MB,内存压力不算太高。在实际使用时,最好对Bitmap设置过期时间,让Redis自动删除不再需要的签到记录以节省内存的消耗。

  • STRLEN key :得出占多少字节 超过8位后自己按照8位一组一byte再扩容

127.0.0.1:6379> SETBIT k3 0 1
(integer) 0
127.0.0.1:6379> SETBIT k3 7 1
(integer) 0
# 占据了一个字节
127.0.0.1:6379> STRLEN k3
(integer) 1
# 不是字符串长度而是占据几个字节,超过8位后自己按照8位一组一byte再扩容
127.0.0.1:6379>
127.0.0.1:6379> SETBIT k3 8 1
(integer) 0
# 再次查看占据了2个字节
127.0.0.1:6379> STRLEN k3
2

  • BITCOUNT key

统计出该key里面含有几个1,

Redis中bitmap、hyperloglog和GEO的实际应用_redis_21

127.0.0.1:6379> SETBIT k5 0 1
(integer) 0
127.0.0.1:6379> SETBIT k5 6 1
(integer) 0
127.0.0.1:6379> SETBIT k5 7 1
(integer) 0
# 统计k5中有几个1
127.0.0.1:6379> BITCOUNT k5
(integer) 3

一年365天,统计全年每天登陆占用多少字节

# 给k6设置几个值
127.0.0.1:6379> SETBIT k6 0 1
(integer) 0
127.0.0.1:6379> SETBIT k6 1 1
(integer) 0
127.0.0.1:6379> SETBIT k6 20 1
(integer) 0
127.0.0.1:6379> SETBIT k6 365 1
(integer) 0
# 统计k6中1的个数,1代表签到
127.0.0.1:6379> BITCOUNT k6
(integer) 4
# 统计占用的字节数
127.0.0.1:6379> STRLEN k6
(integer) 46
127.0.0.1:6379>

  • bitop

BITOP and destKey key1 key2 // 对一个或多个 key 求逻辑并,并将结果保存到 destkey

BITOP or destKey key1 key2 // 对一个或多个 key 求逻辑或,并将结果保存到 destkey

BITOP XOR destKey key1 key2 // 对一个或多个 key 求逻辑异或,并将结果保存到 destkey

BITOP NOT destKey key1 key2 // 对一个或多个 key 求逻辑非,并将结果保存到 destkey

连续2天都签到的用户:

# 创建bitmap 20230318
127.0.0.1:6379> SETBIT 20230318 0 1
(integer) 0
127.0.0.1:6379> SETBIT 20230318 1 1
(integer) 0
127.0.0.1:6379> SETBIT 20230318 2 1
(integer) 0
127.0.0.1:6379> SETBIT 20230318 3 1
(integer) 0
# 创建bitmap 20230319
127.0.0.1:6379> SETBIT 20230319 0 1
(integer) 0
127.0.0.1:6379> SETBIT 20230319 1 1
(integer) 0
127.0.0.1:6379>
# 统计20230318里面有几个1
127.0.0.1:6379> BITCOUNT 20230318
(integer) 4
# 统计20230319里面有几个1
127.0.0.1:6379> BITCOUNT 20230319
(integer) 2
127.0.0.1:6379>
# 求20230318 20230319的并集,将结果保存到了destkey
127.0.0.1:6379> BITOP and destkey 20230318 20230319
(integer) 1
# 统计20230319里面有几个1
127.0.0.1:6379> BITCOUNT destkey
(integer) 2
127.0.0.1:6379>

Redis中bitmap、hyperloglog和GEO的实际应用_spring_22

Redis中bitmap、hyperloglog和GEO的实际应用_redis_23

Redis中bitmap、hyperloglog和GEO的实际应用_spring_24

Redis中bitmap、hyperloglog和GEO的实际应用_spring_25



举报

相关推荐

0 条评论