redis 七. bitmap, hyperloglog, GEO 简单命令及应用场景

一 概述为什么出现这几个数据类型

  1. 亿级数据收集统计流程

数据收集
数据清洗
数据统计

  1. 亿级数据收集统计面临的问题

高并发情况,能不能存的下
亿级数据存储完成后,怎么能方便的随时,瞬间取出
多条件多维度统计速度

  1. 常见统计的类型

聚合统计: 统计多个集合元素的聚合结果,也就是交,差,并等集合统计,例如set类型的交集差集,并集等集合运算
排序统计: list类型,zset(推荐),热点评论等
二值统计: 什么是二值统计,既至于两个值 true\false,签到,上下班打卡,使用bitmap(zset也可以但是要考虑百万千万级别)
基数统计: 去重,例如统计一个集合中不重复的元素,使用hyperloglog

  1. 使用场景(亿级数据收集统计)

统计每天的新增用户,跟第二天的留存用户
商品评论中,统计评论列表中的最新评论(统计+分页)
签到打卡,统计一个月内连续打卡用户
统计网页独立访客的UniqueVistor UV量

  1. 几个专业名词解释

UV: 独立访客Unique Visitor 一般理解为客户端ip,假设两个不同ip一天内都访问了某个网站,则说有两个uv,多次访问去重
PV: 页面浏览量Pae View 假设两个用户分别访问了某个网站3次,则说6个PV
DAU: 日活跃用户量Daily Active User,常用于网站登录(日活),产品的点击量,或者运营情况,去重的
MAU: 月活跃用户量Monthly Active User

二. bitmap

  1. 什么是bitmap: 由0和1状态表现的二进制位的bit数组,节省存储空间,存的快,取的快,bitmap是精确的,bitmap 最大位数是2^32,可以极大的节约存储空间,使用512m内存就可以存储42.9亿字节信息(2 ^32=4294967296),不适合亿级基数统计如果需要统计的有一亿个基数,大约需要100000000/8/1024/1024=12m,如果有1w个亿级比如热搜排行榜,热点视频等,就需要将近120G,所以不适合
    在这里插入图片描述
  2. 优点: 底层是一个二进制位的bit数组,存储了0或1两个状态,跟普通数据类型比还是比较节省内存的
  3. 缺点:
  1. 不存储真实数据,只存储0或1的状态
  2. Bitmap的大小取决于需要记录的数据数量,假设亿级用户每天使用1个亿bitmap,占用128m的内存(10^8/8/1024/1024),10天需要120MB
  1. 简单命令示例
//1.setbit key offset value
//setbit 键 偏移位(偏移量是从0开始算) value只能是0或1

//3.获取指定key的指定偏移量对应的value
getbit key offect

//5.统计该key中为1的个数
bitcount key

//6.获取key1与key2两个key中相同偏移量都是1的个数(destkey 自定义表示"目的key"下方有示例)
bitop and destkey key1 key2
//7.与上面的命令配合使用,获取都是1的偏移量(destkey 自定义与上方的对应表示"目的key"下方有示例)
bitcount destkey

bitmap 的范围查询

  1. bitmap数据结构本身不支持范围查询,可以通过一些技巧来实现
  2. 假设要查询一段时间内的用户签到情况,
//signin:20230501: 时间作为key
//1001: 用户id作为偏移量
//1: 状态值
setbit signin:20230501 1001 1
  1. 通过"bitop and"命令拼接多个key,查询指定时间段内用户的签到情况,例如查询从2023年5月1日到5月7日内用户1001的签到情况,可以使用以下命令
bitop AND signin:20230501 signin:20230502 signin:20230503 signin:20230504 signin:20230505 signin:20230506 signin:20230507

bitmap 底层编码问题

  1. 通过执行"type key"命令查看到bitmap底层实际使用的是String类型
    在这里插入图片描述
  2. 通过redis的"get key"命令获取bitmap的类型数据时返回却是乱码
    在这里插入图片描述
  3. 原因是bitmap 底层是String类型,但是使用的是ascii进行编码的,可以使用"get key"命令,但是返回的是ascii表对应的值

根据 "STRLEN"命令了解 bitmap 的扩容

  1. bitmap 底层使用string字符串,可以使用"strlen key"命令来统计长度,但是要注意该命令按照字节来统计,在bitmap中一个偏移量占用一位,每八位一个byte,
  2. 查看下图发现首先执行"setbit k1 01"添加数据占用1位,然后执行"strlen"获取返回1,再添加>8位获取长度都是1,当执行了"setbit k1 9 1",添加k1的位数>8位(前面未添加的补0),再去获取长度返回2
  3. 一年365天全部签到占多少个字节365/8=46
  4. 总结bitmap每超过8位扩容一次,一次扩容增加8位也就是1个byte
    在这里插入图片描述

使用场景

  1. bitmap 使用场景

日活统计
连续打卡签到
最近一周活跃用户
统计指定用户一年之内的登入天数
某用户365天哪天登录过

  1. 分析签到统计,如果少用户量可以通过mysql实现,如果大数据量,千万级,使用redis的bitmap, 没给用户一天签到用1个bit位,一年也就365个, key为"签到模块+用户id+日期", 签到就存储为1(假设按照年存储用户签到情况,365天需要365/8约等于46bit大小,1000w用户一年需要44mb就可以)
    在这里插入图片描述
    在这里插入图片描述
  2. 假设亿级用户每天使用1个亿bitmap,占用128m的内存(10^8/8/1024/1024),10天需要120MB,实际使用中Bitmap通常会设置过期时间,让redis自动删除不需要的内存开销
bitop 与 bitcount使用案例
  1. 场景案例: 获取连续两天都签到的用户
  1. 注册用户与偏移量进行关系映射:例如0对应用户id0001,1对应用户id0002
  2. 使用bitmap 存储,key为年月日例如"20210515",签到就进行存储
  3. 使用bitop 命令获取连续两个key都为1的偏移量个数
  4. 使用 bitcount+"目的key"命令获取对应的偏移量
  5. 通过映射表拿到偏移量对应的用户id
    在这里插入图片描述
生产使用bitmap优化案例
  1. 需求: 查询用户历史订单,原逻辑直接通过用户信息查询所有订单判断是否存在,由于订单量较大,查询条件较少,查询慢,通过bitmap优化
  1. 用户在我方创建订单时,以酒店编码为key, 用户id为偏移量存储bitmap,最终有多少家酒店就会用多少个bitmap
  2. 查询时分两步走,先获取到所有酒店编码,通过酒店编码查询所有标识用户是否存在历史订单的bitmap,
  3. 如果存在对应的value为1,返回存有当前用户订单的酒店编码,上层再以酒店编码+用户信息作为查询条件查询
  4. 这样增加过滤条件,以酒店为维度,表中针对酒店编码添加索引,减少数据扫描数量,提高查询性能
  5. 防止bitmap数据过大,修改bitmap的key为"业务:酒店编码:年份:月份",修改业务按照月份查询历史订单,最高查询3个月前的
  6. 并且通过bitmap中的bitcount指令,提供了针对酒店入住数据的统计
  1. 用户查询在我方的历史订单时,通过bitmap查询,如果存在返回酒店编码,再通过酒店编码,
bitmap与布隆过滤器
  1. 可以通过bitmap实现布隆过滤器, 比如go-zero中就是,但是与实际的布隆过滤器之间也有不同,比较bitmap与真实的布隆过滤器之间的优缺点
  2. 总结: 当需要判断元素是否存在时,可以使用bitmap,当需要使用较少的内存空间来表示大量元素时,并且可以容忍一定的误判率时,可以使用布隆过滤器
  3. 布隆过滤器的特点:
  1. 空间效率高:布隆过滤器可以使用较少的内存空间来表示大量的元素。
  2. 查询效率高:布隆过滤器在查询元素是否存在时的效率很高,只需要进行几次哈希函数计算即可。
  3. 误判率较高:布隆过滤器在元素存在与否的判断上,存在一定的误判率,即可能将不存在的元素误判为存在。
  4. 不推荐删除元素:因为布隆过滤器使用哈希函数将元素映射到多个比特位上
  1. bitmap与布隆过滤器比较时: bitmap使用一个比特位来表示一个元素的存在与否,与redis的普通数据类型比确实节省了内存空间,但是如果数据量特别大,比如亿级,空间占用还是很大的,但是bitmap是精确的,不会存在误判

二. hyperloglog

  1. 什么是hyperloglog: 去重统计功能的基数估算发就是hyperloglog,
  2. 特点:在输入元素的数量或体积非常大时,计算基数所需要的空间总是固定的,并且是很小的,每个hyperloglog键需要12kb内存可以计算2^64个不同元素的基数,这和基数基数元素越多耗费内存越多的集合形成鲜明对比,但是hyperloglog只会根据元素来计算基数,不会存储元素本身,所以不能像其它类型一样返回各个元素
  3. hyperloglog是概率算法,是牺牲准确率换区空间的,对于对精度要求不高的情况下可以使用,因为概率算法本身不直接存储数据本身,能保证误差在一定范围内,又不占用空间,误差在0.81%左右(1.04/sqrt(m)) m表示redis使用寄存器的个数16384个等于0.81%
  4. 思考: 上面学过bitmap,学过set为什么不用这两个数据类型来统计基数,因为这里考虑的是大数据量千万级或亿级情况下,

bitmap用bit数组来表示元素是否出现,每个元素对应一位,需要n个bit,基数统计只需要获取存在的bit数组和新加入的元素按位计算即可,能够减少内存占用迅速响应,但是:如果需要统计的有一亿个基数,大约需要100000000/8/1024/1024=12m,如果有1w个亿级比如热搜排行榜,热点视频等,就需要将近120G,所以不适合,注意点bitmap是精确的
为什么不用redis的hash类型: 按照ipv4结构来说,ipv4一个地址最多占15个字节,像京东天猫这种每天访问量在亿级以上1.5亿字节=2g会打爆redis

  1. 小总结: hyperloglog: 只进行不重复基数统计,不是集合,也不保存数据本身,值记录数量,牺牲准确率换取空间,误差在0.81%左右
  2. hyperloglog 命令
//1.添加,var可以看为是key,element为对应该key的数据可以为空,可以是多个
PFADD var element element....emelent

//2.获取该var对应数据的个数(去重的)
PFCOUNT var

//3.合并多个var到新key dst自定义
PFMERGE dst var1 var2....varN
//4.获取上面dst中保存的个数
PFCOUNT dst

使用场景

  1. 统计某个网站的UV,统计某个文章的UV,例如统计京东的UV量, key为"jd:uv日期",后面的1,2,3,假设是访问用户的ip,黄色部分计算jd日期为uv1的访问量返回3,蓝色部分计算uv1,uv2两天的uv量量返回6(因为uv1与与uv2中有重复的)
    在这里插入图片描述
  2. 统计一个网站uv 代码
package com.redis.test.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
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 java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@Api(description = "获取网站UV的Redis统计方案")
@RestController
public class HyperLogLogController {

    @Autowired
    private RedisTemplate redisTemplate;

    private static final String uvKey = "uv:";

    @ApiOperation("获得ip去重复后的首页访问量,总数统计")
    @RequestMapping(value = "/getUV", method = RequestMethod.GET)
    public long getUV(String date) {
        return redisTemplate.opsForHyperLogLog().size(uvKey+date);
    }

    @ApiOperation("记录用户访问uv")
    @RequestMapping(value = "/addUV", method = RequestMethod.GET)
    public Integer addUV(String ip) {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        String timeStr = LocalDate.now().format(dateTimeFormatter);
        redisTemplate.opsForHyperLogLog().add(uvKey + timeStr, ip);
        return 0;
    }
}
  1. 用户搜索关键词的数量
  2. 统计用户每天搜索的不同词条数

三. GEO

  1. 需求: 附近的人,打车软件附近的车辆,附近的店铺等等,经度, 纬度(-90, 90)跟地理位置相关的
  2. 获取举例我500米内的车辆需求,传统模式下,例如使用MySql

高并发下性能问题
查询的是一个范围矩形访问,而不是以我为中心r公里为半径的原型访问
精准度问题,我们要的是一个地球坐标,这种矩形计算在长距离上会有很大误差

  1. 核心点: 将三维的地球转换为二维的坐标,将二维的坐标转换为一维的点块,最终将一维的点块转换为二进制base32编码存储
  2. 简单解释经纬度:

经度:(-180, 180),东经为正数,西经为负数
纬度:(-90, 90),南纬为正数,北纬为负数

命令

//1.添加经纬度坐标
GEOADD
//2.返回经纬度
GEOPOS
//3.返回坐标的geohash表示
GEOHASH
//4.两个位置之间的举例
GEODIST
//5.以给定的经纬度为中心,返回与该经纬度距离不超过给定距离的所有位置元素
GEORADIUS
//6.以给定的元素为中心(元素可能重复,假设存储时 经纬度 天安门,这个天安门就是元素值)
//获取距离该元素不超过给定范围的元素
GEORADIUSBYMEMBER
GEOADD命令使用示例,与GEO数据类型问题
  1. 插入天安门,故宫,长城经纬度,并通过示例与"type key"命令查看存储的GEO数据类型为ZSET,然后使用ZSET的命令获取该key值"ZRANGE key start stop",返回的可能是乱码,需要执行" – raw"命令后,再次执查询会返回(“天安门 故宫 长城”)
    在这里插入图片描述
  2. 可以这样理解zset类型是"key 分数 值"而 geo类型是 “key 经纬度 值”
GEOPOS命令使用示例
  1. 获取天安门,故宫,长城经纬度
    在这里插入图片描述
GEOHASH 命令使用示例
  1. 获取天安门的hash编码
    在这里插入图片描述
GEODIST 命令使用示例
  1. 计算两个位置的举例
    在这里插入图片描述
GEORADIUS 命令使用示例
  1. 以给定的经纬度为中心,返回键包含的位置元素当中,与该键距离不超过给定距离的所有位置元素
  2. 其中有几个附加命令
    在这里插入图片描述
  3. 示例:查看距离我不超过10mk的10个地标建筑
    在这里插入图片描述
GEORADIUSBYMEMBER 命令使用示例

在这里插入图片描述

案例

import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
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 java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class GeoController {
    public  static final String CITY ="city";

    @Autowired
    private RedisTemplate redisTemplate;

    @ApiOperation("新增城市经纬度坐标")
    @RequestMapping(value = "/geoadd",method = RequestMethod.POST)
    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();
    }

    /**
     * 
     * @param member 地理位置名称
     * @return
     */
    @ApiOperation("获取地理位置的坐标")
    @RequestMapping(value = "/geopos",method = RequestMethod.GET)
    public Point position(String member) {
        //获取经纬度坐标
        List<Point> list= this.redisTemplate.opsForGeo().position(CITY,member);
        return list.get(0);
    }

    @ApiOperation("geohash算法生成的base32编码值")
    @RequestMapping(value = "/geohash",method = RequestMethod.GET)
    public String hash(String member) {
        //geohash算法生成的base32编码值
        List<String> list= this.redisTemplate.opsForGeo().hash(CITY,member);
        return list.get(0);
    }

    @ApiOperation("计算两个位置之间的距离")
    @RequestMapping(value = "/geodist",method = RequestMethod.GET)
    public Distance distance(String member1, String member2) {
        Distance distance= this.redisTemplate.opsForGeo().distance(CITY,member1,member2, RedisGeoCommands.DistanceUnit.KILOMETERS);
        return distance;
    }

    /**
     * 通过经度,纬度查找附近的
     * 北京王府井位置116.418017,39.914402,这里为了方便讲课,故意写死
     */
    @ApiOperation("通过经度,纬度查找附近的")
    @RequestMapping(value = "/georadius",method = RequestMethod.GET)
    public GeoResults radiusByxy() {
        //这个坐标是北京王府井位置
        Circle circle = new Circle(116.418017, 39.914402, Metrics.MILES.getMultiplier());
        //返回50条
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(10);
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,circle, args);
        return geoResults;
    }

    /**
     * 通过地方查找附近
     */
    @ApiOperation("通过地方查找附近")
    @RequestMapping(value = "/georadiusByMember",method = RequestMethod.GET)
    public GeoResults radiusByMember() {
        String member="天安门";
        //返回50条
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(10);
        //半径10公里内
        Distance distance=new Distance(10, Metrics.KILOMETERS);
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,member, distance,args);
        return geoResults;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值